Merge pull request #11 from sjackson0109/feature/csharp-migration

Feature/csharp migration
pull/4062/head
Simon Jackson 1 month ago committed by GitHub
commit c3028136b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

25
.github/CODEOWNERS vendored

@ -0,0 +1,25 @@
# CODEOWNERS
# GitHub automatically requests a review from the listed owners whenever a PR
# modifies files matching the pattern. More specific rules override less specific
# ones (last match wins), so list broad patterns first.
#
# Docs: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Catch-all: maintainer reviews everything
* @sjackson0109
# C++ DLL source
src-x86-x64-Fusix/ @sjackson0109
# C# tools (installer, configurator, checker, offset-finder)
src-csharp/ @sjackson0109
# WiX MSI packaging
msi/ @sjackson0109
# CI/CD pipelines
.github/ @sjackson0109
# Documentation
docs/ @sjackson0109
README.md @sjackson0109

@ -58,6 +58,7 @@ body:
options:
- x64 (64-bit)
- x86 (32-bit)
- ARM64
validations:
required: true

@ -0,0 +1,34 @@
version: 2
updates:
# Keep GitHub Actions pinned to their latest minor/patch releases
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 10
groups:
actions-minor:
patterns: ["*"]
# Keep .NET SDK NuGet dependencies up to date (for src-csharp/)
- package-ecosystem: nuget
directory: /src-csharp
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 10
# Monitor git submodules for upstream releases
# Covers: src-csharp/RDPOffsetFinder (llccd/RDPWrapOffsetFinder)
# src-csharp/RDPOffsetFinder/zydis (zyantific/zydis)
# Note: gitsubmodule ecosystem is currently in Dependabot beta.
# Enable once the feature is available for the repository, or track
# submodule versions manually via the pinned SHA in .gitmodules.
- package-ecosystem: gitsubmodule
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5

@ -0,0 +1,776 @@
name: Build and Release
on:
push:
branches: [main, master]
paths:
- 'msi/rdpwrap.ini'
- 'msi/rdpwrap-arm-kb.ini'
- 'msi/**'
- 'src-x86-x64-Fusix/**'
- 'src-csharp/**'
- '.github/workflows/build-and-release.yml'
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g. v2026.4.2). Leave blank to auto-generate from date.'
required: false
default: ''
prerelease:
description: 'Mark as pre-release'
type: boolean
required: false
default: false
concurrency:
group: release
cancel-in-progress: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: write
# ────────────────────────────────────────────────────────────────────────────
# Job dependency / parallelism map
#
# build-dll ───────────────► build-csharp ──────────► build-msi ──┐
# build-offsetfinder ───────────────────────────────────────────► release
# download-sergiye ───────────────────────────────────────────► release
# build-dll ─┘
# ────────────────────────────────────────────────────────────────────────────
jobs:
# ── Job 1: Build rdpwrap.dll (x64, x86, ARM64) ────────────────────────────
# No job dependencies — starts immediately.
build-dll:
name: Build rdpwrap DLL
runs-on: windows-2022
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v3
- name: Build rdpwrap.dll (x64, Win32, ARM64)
shell: pwsh
run: |
foreach ($platform in @('x64', 'Win32', 'ARM64')) {
msbuild src-x86-x64-Fusix/RDPWrap.vcxproj `
/p:Configuration=Release `
/p:Platform="$platform" `
/p:PlatformToolset=v143 `
/p:WindowsTargetPlatformVersion=10.0 `
/v:minimal
}
Copy-Item src-x86-x64-Fusix/Release/x64/rdpwrap.dll .\rdpwrap_x64.dll
Copy-Item src-x86-x64-Fusix/Release/x86/rdpwrap.dll .\rdpwrap_x86.dll
Copy-Item src-x86-x64-Fusix/Release/arm64/rdpwrap.dll .\rdpwrap_arm64.dll
Write-Host "Built DLLs:"
Get-Item .\rdpwrap_x64.dll, .\rdpwrap_x86.dll, .\rdpwrap_arm64.dll | Format-Table Name, Length
- name: Upload rdpwrap DLLs
uses: actions/upload-artifact@v4
with:
name: rdpwrap-dlls
path: |
rdpwrap_x64.dll
rdpwrap_x86.dll
rdpwrap_arm64.dll
if-no-files-found: error
# ── Job 2: Build RDPWrapOffsetFinder (x64, x86) — parallel to build-dll ───
# No job dependencies — starts immediately.
build-offsetfinder:
name: Build OffsetFinder
runs-on: windows-2022
outputs:
finder_ver: ${{ steps.finder.outputs.finder_ver }}
steps:
- name: Checkout repository (with submodules)
uses: actions/checkout@v6
with:
submodules: recursive
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v3
- name: Build RDPWrapOffsetFinder from submodule
id: finder
shell: pwsh
run: |
$version = (git -C src-csharp/RDPOffsetFinder describe --tags --always).Trim()
Write-Host "Submodule version: $version"
foreach ($cfg in @(
@{ Platform = 'x64'; ZydisCfg = 'Release MD DLL'; ZydisBin = 'ReleaseX64'; Arch = 'x64' }
@{ Platform = 'Win32'; ZydisCfg = 'Release MD DLL'; ZydisBin = 'ReleaseX86'; Arch = 'x86' }
)) {
# Build Zydis DLL
msbuild src-csharp/RDPOffsetFinder/zydis/msvc/zydis/Zydis.vcxproj `
/p:Configuration="$($cfg.ZydisCfg)" `
/p:Platform="$($cfg.Platform)" `
/p:PlatformToolset=v143 `
/v:minimal
# Build RDPWrapOffsetFinder
msbuild src-csharp/RDPOffsetFinder/RDPWrapOffsetFinder/RDPWrapOffsetFinder.vcxproj `
/p:Configuration=Release `
/p:Platform="$($cfg.Platform)" `
/p:PlatformToolset=v143 `
/v:minimal
$arch = $cfg.Arch
$exeSrc = "src-csharp/RDPOffsetFinder/RDPWrapOffsetFinder/$($cfg.Platform)/Release/RDPWrapOffsetFinder.exe"
$dllSrc = "src-csharp/RDPOffsetFinder/zydis/msvc/bin/$($cfg.ZydisBin)/Zydis.dll"
Copy-Item $exeSrc ".\RDPWrapOffsetFinder_$arch.exe"
Copy-Item $dllSrc ".\Zydis_$arch.dll"
}
Write-Host "Built finder tools (version $version):"
Get-Item .\RDPWrapOffsetFinder_*.exe, .\Zydis_*.dll | Format-Table Name, Length
echo "finder_ver=$version" >> $env:GITHUB_OUTPUT
- name: Upload OffsetFinder binaries
uses: actions/upload-artifact@v4
with:
name: offsetfinder-binaries
path: |
RDPWrapOffsetFinder_x64.exe
RDPWrapOffsetFinder_x86.exe
Zydis_x64.dll
Zydis_x86.dll
if-no-files-found: error
# ── Job 3: Download rdpWrapper GUI from sergiye — parallel to build-dll ────
# No job dependencies — starts immediately.
download-sergiye:
name: Download sergiye rdpWrapper
runs-on: windows-2022
outputs:
wrapper_ver: ${{ steps.wrapper.outputs.wrapper_ver }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Download rdpWrapper (sergiye)
id: wrapper
shell: pwsh
run: |
$apiUrl = "https://api.github.com/repos/sergiye/rdpWrapper/releases/latest"
$release = Invoke-RestMethod -Uri $apiUrl -Headers @{ "User-Agent" = "rdpwrap-ci" }
foreach ($arch in @('x64', 'x86')) {
$asset = $release.assets |
Where-Object { $_.name -eq "rdpWrapper_$arch.exe" } |
Select-Object -First 1
if (-not $asset) {
throw "Could not find rdpWrapper_$arch.exe in sergiye release $($release.tag_name)"
}
Write-Host "Downloading $($asset.browser_download_url)"
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile ".\rdpWrapper_$arch.exe"
}
Write-Host "Wrapper exes:"
Get-Item .\rdpWrapper_x64.exe, .\rdpWrapper_x86.exe | Format-Table Name, Length
echo "wrapper_ver=$($release.tag_name)" >> $env:GITHUB_OUTPUT
# ── SHA-256 audit log for third-party binaries ──────────────────────────
# Records the hash of every downloaded sergiye binary in the workflow log.
# This provides a tamper-evident paper trail without blocking the release.
- name: Audit SHA-256 of downloaded binaries
shell: pwsh
run: |
$files = Get-Item .\rdpWrapper_x64.exe, .\rdpWrapper_x86.exe
Write-Host "SHA-256 audit (third-party downloads from sergiye/rdpWrapper):"
foreach ($f in $files) {
$hash = (Get-FileHash $f.FullName -Algorithm SHA256).Hash
Write-Host " $($f.Name) $hash"
}
# ── Verify hashes against pinned values in tools/sergiye-hashes.json ────
# If the file contains a non-empty 'release' key, hashes must match exactly.
# If 'release' is empty this step is skipped (bootstrap scenario).
- name: Verify sergiye binary hashes
shell: pwsh
run: |
$pinFile = 'tools/sergiye-hashes.json'
$pin = Get-Content $pinFile -Raw | ConvertFrom-Json
if ([string]::IsNullOrEmpty($pin.release)) {
Write-Host "Hash pinning not yet configured (tools/sergiye-hashes.json 'release' is empty) — skipping."
Write-Host "To enable pinning: download the binaries, compute hashes with Get-FileHash, and update $pinFile."
} else {
Write-Host "Verifying against pinned release: $($pin.release)"
$pass = $true
foreach ($name in @('rdpWrapper_x64.exe', 'rdpWrapper_x86.exe')) {
$expected = $pin.$name
$actual = (Get-FileHash ".\$name" -Algorithm SHA256).Hash
if ($actual -ne $expected) {
Write-Error "Hash mismatch for $name!"
Write-Error " Expected: $expected"
Write-Error " Actual: $actual"
$pass = $false
} else {
Write-Host " $name OK ($actual)"
}
}
if (-not $pass) { throw "One or more sergiye binary hashes did not match the pinned values." }
}
- name: Upload sergiye wrappers
uses: actions/upload-artifact@v4
with:
name: sergiye-wrappers
path: |
rdpWrapper_x64.exe
rdpWrapper_x86.exe
if-no-files-found: error
# ── Job 4: Build C# tools (FD + SC) — waits for build-dll ────────────────
# Waits for build-dll so the DLLs can be embedded into single-file EXEs.
build-csharp:
name: Build C# Tools
runs-on: windows-2022
needs: [build-dll]
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Download rdpwrap DLLs
uses: actions/download-artifact@v4
with:
name: rdpwrap-dlls
path: .
# Stage embedded resources for RDPWInst offline/bundled mode.
# RDPWInst.csproj embeds these files when present (Condition="Exists(...)").
# Copying them here ensures the single-file EXE can install without network.
- name: Stage RDPWInst embedded resources
shell: pwsh
run: |
$res = 'src-csharp/RDPWInst/Resources'
New-Item -ItemType Directory -Path $res -Force | Out-Null
Copy-Item .\rdpwrap_x86.dll "$res\rdpw32.dll" -Force
Copy-Item .\rdpwrap_x64.dll "$res\rdpw64.dll" -Force
Copy-Item msi\rdpwrap.ini "$res\rdpwrap.ini" -Force
Copy-Item LICENSE "$res\license.txt" -Force
Write-Host "Staged resources:"
Get-ChildItem $res | Format-Table Name, Length
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.x'
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/Directory.Build.props', '**/*.csproj') }}
restore-keys: |
nuget-${{ runner.os }}-
# Compute version once so it's consistent across all publish and sign steps
- name: Get build version
id: build_ver
shell: pwsh
run: |
$v = Get-Date -Format "yyyy.M.d"
echo "version=$v" >> $env:GITHUB_OUTPUT
Write-Host "Build version: $v"
# ── Build framework-dependent variants ──────────────────────────────────
- name: Publish C# tools (x64, x86, arm64)
shell: pwsh
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
run: |
$projects = @('RDPWInst', 'RDPConf', 'RDPCheck')
$rids = [ordered]@{ x64 = 'win-x64'; x86 = 'win-x86'; arm64 = 'win-arm64' }
$ver = '${{ steps.build_ver.outputs.version }}'
foreach ($proj in $projects) {
foreach ($plat in $rids.Keys) {
$rid = $rids[$plat]
dotnet publish "src-csharp/$proj/$proj.csproj" `
-c Release `
-r $rid `
-p:Platform=$plat `
-p:PublishSingleFile=true `
-p:SelfContained=false `
-p:Version=$ver `
-p:AssemblyVersion="$ver.0" `
-p:FileVersion="$ver.0" `
--output "dist_cs\$plat\$proj"
Copy-Item "dist_cs\$plat\$proj\$proj.exe" ".\${proj}_${plat}.exe"
}
}
Write-Host "C# tool executables:"
Get-Item .\RDPWInst_*.exe, .\RDPConf_*.exe, .\RDPCheck_*.exe | Format-Table Name, Length
# ── Build self-contained variants (no .NET runtime required) ─────────────
# Self-contained exes are larger (~60 MB) but work without a .NET install.
# Suffixed with _sc to distinguish from the framework-dependent versions.
- name: Publish self-contained C# tools (x64, x86, arm64)
shell: pwsh
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
run: |
$projects = @('RDPWInst', 'RDPConf', 'RDPCheck')
$rids = [ordered]@{ x64 = 'win-x64'; x86 = 'win-x86'; arm64 = 'win-arm64' }
$ver = '${{ steps.build_ver.outputs.version }}'
foreach ($proj in $projects) {
foreach ($plat in $rids.Keys) {
$rid = $rids[$plat]
dotnet publish "src-csharp/$proj/$proj.csproj" `
-c Release `
-r $rid `
-p:Platform=$plat `
-p:PublishSingleFile=true `
-p:SelfContained=true `
-p:Version=$ver `
-p:AssemblyVersion="$ver.0" `
-p:FileVersion="$ver.0" `
--output "dist_cs_sc\$plat\$proj"
Copy-Item "dist_cs_sc\$plat\$proj\$proj.exe" ".\${proj}_${plat}_sc.exe"
}
}
Write-Host "Self-contained executables:"
Get-Item .\RDPWInst_*_sc.exe, .\RDPConf_*_sc.exe, .\RDPCheck_*_sc.exe | Format-Table Name, Length
# ── Optional: code-sign all C# executables ──────────────────────────────
# Enable by setting repository variable USE_CERT_SIGNING=true and adding
# secrets CODESIGN_CERT_BASE64 (PFX as base64) and CODESIGN_CERT_PASSWORD.
- name: Sign C# executables
if: vars.USE_CERT_SIGNING == 'true'
env:
CODESIGN_CERT_BASE64: ${{ secrets.CODESIGN_CERT_BASE64 }}
CODESIGN_CERT_PASSWORD: ${{ secrets.CODESIGN_CERT_PASSWORD }}
shell: pwsh
run: |
$pfxPath = "$env:RUNNER_TEMP\codesign.pfx"
[IO.File]::WriteAllBytes($pfxPath,
[Convert]::FromBase64String($env:CODESIGN_CERT_BASE64))
$signtool = (Resolve-Path "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" |
Sort-Object | Select-Object -Last 1).Path
# Sign both framework-dependent and self-contained variants (glob picks up all _*.exe files)
$exes = Get-Item .\RDPWInst_*.exe, .\RDPConf_*.exe, .\RDPCheck_*.exe
foreach ($exe in $exes) {
& $signtool sign /fd SHA256 /f $pfxPath `
/p $env:CODESIGN_CERT_PASSWORD `
/tr http://timestamp.digicert.com /td SHA256 $exe.FullName
}
Remove-Item $pfxPath -Force
- name: Upload C# tools
uses: actions/upload-artifact@v4
with:
name: csharp-tools
path: |
RDPWInst_*.exe
RDPConf_*.exe
RDPCheck_*.exe
if-no-files-found: error
# ── Job 5: Build MSI packages — waits for build-csharp (implies build-dll) ─
build-msi:
name: Build MSI Packages
runs-on: windows-2022
needs: [build-csharp]
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Download rdpwrap DLLs
uses: actions/download-artifact@v4
with:
name: rdpwrap-dlls
path: .
- name: Download C# tools
uses: actions/download-artifact@v4
with:
name: csharp-tools
path: .
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.x'
- name: Cache NuGet packages (WiX restore)
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('msi/global.json', 'msi/*.wixproj') }}
restore-keys: |
nuget-${{ runner.os }}-
# Get version (same formula as build-csharp for filename consistency)
- name: Get build version
id: build_ver
shell: pwsh
run: |
$v = Get-Date -Format "yyyy.M.d"
echo "version=$v" >> $env:GITHUB_OUTPUT
Write-Host "Build version: $v"
# ── Build MSI installers (WiX v5, x64 / x86 / arm64) ──────────────────
- name: Build MSI packages
shell: pwsh
run: |
$ver = '${{ steps.build_ver.outputs.version }}'
foreach ($arch in @('x64', 'x86', 'arm64')) {
# Stage arch-specific inputs next to the .wixproj
Copy-Item ".\rdpwrap_$arch.dll" "msi\rdpwrap_$arch.dll" -Force
Copy-Item ".\RDPWInst_$arch.exe" "msi\RDPWInst_$arch.exe" -Force
Copy-Item ".\RDPConf_$arch.exe" "msi\RDPConf_$arch.exe" -Force
Copy-Item ".\RDPCheck_$arch.exe" "msi\RDPCheck_$arch.exe" -Force
dotnet build msi/RDPWInst.wixproj `
-c Release `
/p:Platform=$arch `
/p:OutputName="RDPWrapper-$arch" `
/p:PackageVersion=$ver `
/p:OutputPath="$PWD/msi_out/$arch"
if ($LASTEXITCODE -ne 0) { throw "WiX build failed for $arch" }
$msiSrc = Get-Item "msi_out\$arch\RDPWrapper-$arch.msi" -ErrorAction SilentlyContinue
if (-not $msiSrc) {
$msiSrc = Get-ChildItem "msi\bin" -Recurse -Filter "RDPWrapper-$arch.msi" |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
}
if (-not $msiSrc) { throw "Could not locate RDPWrapper-$arch.msi after WiX build" }
Copy-Item $msiSrc.FullName ".\RDPWrapper-${ver}-$arch.msi"
Write-Host "Produced: RDPWrapper-${ver}-$arch.msi ($([math]::Round($msiSrc.Length/1MB,1)) MB)"
}
Write-Host "MSI packages:"
Get-Item .\RDPWrapper-*-*.msi | Format-Table Name, Length
# ── Optional: sign the MSI packages ────────────────────────────────────
# Enable by setting repository variable USE_CERT_SIGNING=true and adding
# secrets CODESIGN_CERT_BASE64 (PFX as base64) and CODESIGN_CERT_PASSWORD.
- name: Sign MSI packages
if: vars.USE_CERT_SIGNING == 'true'
env:
CODESIGN_CERT_BASE64: ${{ secrets.CODESIGN_CERT_BASE64 }}
CODESIGN_CERT_PASSWORD: ${{ secrets.CODESIGN_CERT_PASSWORD }}
shell: pwsh
run: |
$pfxPath = "$env:RUNNER_TEMP\codesign.pfx"
[IO.File]::WriteAllBytes($pfxPath,
[Convert]::FromBase64String($env:CODESIGN_CERT_BASE64))
$signtool = (Resolve-Path "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" |
Sort-Object | Select-Object -Last 1).Path
foreach ($msi in (Get-Item .\RDPWrapper-*-*.msi)) {
& $signtool sign /fd SHA256 /f $pfxPath `
/p $env:CODESIGN_CERT_PASSWORD `
/tr http://timestamp.digicert.com /td SHA256 `
$msi.FullName
}
Remove-Item $pfxPath -Force
- name: Upload MSI packages
uses: actions/upload-artifact@v4
with:
name: msi-packages
path: RDPWrapper-*-*.msi
if-no-files-found: error
# ── Job 6: Publish GitHub Release — waits for all build jobs ──────────────
release:
name: Publish Release
runs-on: windows-2022
needs: [build-dll, build-csharp, build-offsetfinder, download-sergiye, build-msi]
# Attach to the 'release' GitHub Environment so repository admins can add
# required-reviewer gates in Settings → Environments → release.
# If the environment doesn't exist yet it will be created without gates.
environment: release
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Download rdpwrap DLLs
uses: actions/download-artifact@v4
with:
name: rdpwrap-dlls
path: .
- name: Download C# tools
uses: actions/download-artifact@v4
with:
name: csharp-tools
path: .
- name: Download OffsetFinder binaries
uses: actions/download-artifact@v4
with:
name: offsetfinder-binaries
path: .
- name: Download sergiye wrappers
uses: actions/download-artifact@v4
with:
name: sergiye-wrappers
path: .
- name: Download MSI packages
uses: actions/download-artifact@v4
with:
name: msi-packages
path: .
- name: Verify all release assets are present
shell: pwsh
run: |
$required = @(
'rdpwrap_x64.dll', 'rdpwrap_x86.dll', 'rdpwrap_arm64.dll',
'RDPWInst_x64.exe', 'RDPWInst_x86.exe', 'RDPWInst_arm64.exe',
'RDPConf_x64.exe', 'RDPConf_x86.exe', 'RDPConf_arm64.exe',
'RDPCheck_x64.exe', 'RDPCheck_x86.exe', 'RDPCheck_arm64.exe',
'RDPWrapOffsetFinder_x64.exe', 'RDPWrapOffsetFinder_x86.exe',
'Zydis_x64.dll', 'Zydis_x86.dll',
'rdpWrapper_x64.exe', 'rdpWrapper_x86.exe'
)
$missing = $required | Where-Object { -not (Test-Path $_) }
if ($missing) {
throw "Missing required release assets: $($missing -join ', ')"
}
Write-Host "All required assets present."
Get-ChildItem *.exe, *.dll, *.msi | Format-Table Name, Length
# ── Extract the date string used for tagging and the updated field ───────
- name: Get release metadata
id: meta
shell: pwsh
run: |
$date = Get-Date -Format "yyyy.MM.dd.HHmm"
$stamp = Get-Date -Format "yyyy-MM-dd HH:mm UTC"
# Pull the Updated= line out of the INI to surface in release notes
$iniContent = Get-Content msi/rdpwrap.ini -Raw
$iniDate = if ($iniContent -match 'Updated=([^\r\n]+)') { $Matches[1] } else { 'unknown' }
$inputTag = '${{ inputs.tag }}'.Trim()
$tagName = if ($inputTag -ne '') { $inputTag } else { "ini-$date" }
$outputName = if ($inputTag -ne '') { "Release $inputTag" } else { "INI Update $date" }
echo "date=$date" >> $env:GITHUB_OUTPUT
echo "stamp=$stamp" >> $env:GITHUB_OUTPUT
echo "inidate=$iniDate" >> $env:GITHUB_OUTPUT
echo "tag_name=$tagName" >> $env:GITHUB_OUTPUT
echo "release_name=$outputName" >> $env:GITHUB_OUTPUT
# ── Validate the INI has required sections and mandatory per-version keys ─
- name: Validate INI
shell: pwsh
run: |
$ini = Get-Content msi/rdpwrap.ini -Raw
$lines = Get-Content msi/rdpwrap.ini
# 1) Check mandatory top-level sections exist
foreach ($section in @('[Main]', '[SLPolicy]', '[PatchCodes]')) {
if ($ini -notmatch [regex]::Escape($section)) {
throw "INI validation failed: missing required section $section"
}
}
# 2) Parse every Windows-version section and verify it contains the
# mandatory keys LocalOnlyPatch and SLInitHook.
$sectionKeys = [System.Collections.Generic.Dictionary[string, System.Collections.Generic.List[string]]]::new()
$currentSection = $null
foreach ($line in $lines) {
if ($line -match '^\[(\d[\d\.]+)\]') {
$currentSection = $Matches[1]
$sectionKeys[$currentSection] = [System.Collections.Generic.List[string]]::new()
} elseif ($null -ne $currentSection -and $line -match '^([A-Za-z][A-Za-z0-9_]*)[\s]*=') {
$sectionKeys[$currentSection].Add($Matches[1])
}
}
$mandatoryKeys = @('LocalOnlyPatch', 'SLInitHook')
$errors = [System.Collections.Generic.List[string]]::new()
foreach ($sec in $sectionKeys.Keys) {
foreach ($key in $mandatoryKeys) {
if (-not $sectionKeys[$sec].Contains($key)) {
$errors.Add(" Section [$sec] is missing mandatory key: $key")
}
}
}
if ($errors.Count -gt 0) {
Write-Error "INI mandatory-key check failed ($($errors.Count) issue(s)):"
$errors | ForEach-Object { Write-Error $_ }
throw "INI validation failed — see errors above."
}
$sectionCount = $sectionKeys.Count
Write-Host "INI is valid. Windows-version sections: $sectionCount (all contain LocalOnlyPatch + SLInitHook)"
# ── Auto-generate changelog from merged PRs since last release ───────────
- name: Generate changelog
id: changelog
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
# Get the tag of the previous release so we can scope the PR list
$prevTag = (gh release list --limit 1 --json tagName --jq '.[0].tagName' 2>$null) ?? ''
if ($prevTag) {
Write-Host "Generating changelog since: $prevTag"
$since = gh release view $prevTag --json publishedAt --jq .publishedAt
$prs = gh pr list --state merged --base main --search "merged:>=$since" `
--json number,title,author --limit 50 | ConvertFrom-Json
} else {
Write-Host "No previous release found; skipping per-PR changelog."
$prs = @()
}
if ($prs.Count -gt 0) {
$lines = $prs | ForEach-Object { "- #$($_.number) $($_.title) (@$($_.author.login))" }
$body = $lines -join "`n"
} else {
$body = "_No merged pull requests found since the previous release._"
}
# Write multi-line output safely
$delim = [guid]::NewGuid().ToString('N')
echo "prs<<$delim" >> $env:GITHUB_OUTPUT
echo $body >> $env:GITHUB_OUTPUT
echo $delim >> $env:GITHUB_OUTPUT
# ── Assemble the three user-facing distribution bundles ──────────────────
- name: Create distribution bundles
shell: pwsh
run: |
# RDPWrapper.zip - complete install package (all arches, framework-dependent)
$d = ".\bundle_wrapper"
New-Item -ItemType Directory -Path $d -Force | Out-Null
Copy-Item .\rdpwrap_x64.dll "$d\rdpwrap_x64.dll"
Copy-Item .\rdpwrap_x86.dll "$d\rdpwrap_x86.dll"
Copy-Item .\rdpwrap_arm64.dll "$d\rdpwrap_arm64.dll"
Copy-Item .\rdpWrapper_x64.exe "$d\rdpWrapper_x64.exe"
Copy-Item .\rdpWrapper_x86.exe "$d\rdpWrapper_x86.exe"
# Framework-dependent C# tools (require .NET 10 Desktop Runtime)
foreach ($proj in @('RDPWInst', 'RDPConf', 'RDPCheck')) {
foreach ($arch in @('x64', 'x86', 'arm64')) {
$src = ".\${proj}_${arch}.exe"
if (Test-Path $src) { Copy-Item $src "$d\${proj}_${arch}.exe" }
}
}
Copy-Item msi\rdpwrap.ini "$d\rdpwrap.ini"
Compress-Archive -Path "$d\*" -DestinationPath ".\RDPWrapper.zip" -Force
Remove-Item $d -Recurse -Force
# RDPWrapper-SelfContained.zip - apps bundled with .NET runtime (~60 MB each)
$sc = ".\bundle_sc"
New-Item -ItemType Directory -Path $sc -Force | Out-Null
foreach ($proj in @('RDPWInst', 'RDPConf', 'RDPCheck')) {
foreach ($arch in @('x64', 'x86', 'arm64')) {
$src = ".\${proj}_${arch}_sc.exe"
if (Test-Path $src) { Copy-Item $src "$sc\${proj}_${arch}_sc.exe" }
}
}
Copy-Item msi\rdpwrap.ini "$sc\rdpwrap.ini"
Compress-Archive -Path "$sc\*" -DestinationPath ".\RDPWrapper-SelfContained.zip" -Force
Remove-Item $sc -Recurse -Force
# RDPWrapOffsetFinder.zip - offset finder with per-arch subfolders
$f = ".\bundle_finder"
foreach ($arch in @('x64', 'x86')) {
$dir = "$f\$arch"
New-Item -ItemType Directory -Path $dir -Force | Out-Null
Copy-Item ".\RDPWrapOffsetFinder_$arch.exe" "$dir\RDPWrapOffsetFinder.exe"
Copy-Item ".\Zydis_$arch.dll" "$dir\Zydis.dll"
}
Compress-Archive -Path "$f\*" -DestinationPath ".\RDPWrapOffsetFinder.zip" -Force
Remove-Item $f -Recurse -Force
Write-Host "Distribution bundles:"
Get-Item .\RDPWrapper.zip, .\RDPWrapper-SelfContained.zip, .\RDPWrapOffsetFinder.zip | Format-Table Name, Length
# ── Create/update a versioned GitHub Release with all assets ─────────────
- name: Publish release
# Pinned to v2 tag SHA for supply-chain safety.
# Bump this SHA when upgrading: https://github.com/softprops/action-gh-release/releases
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
with:
tag_name: "${{ steps.meta.outputs.tag_name }}"
name: "${{ steps.meta.outputs.release_name }}"
prerelease: ${{ inputs.prerelease || false }}
make_latest: true
body: |
## RDP Wrapper - Automated Release
| Field | Value |
|---|---|
| INI `Updated` date | `${{ steps.meta.outputs.inidate }}` |
| Published | ${{ steps.meta.outputs.stamp }} |
| rdpWrapper (sergiye) | ${{ needs.download-sergiye.outputs.wrapper_ver }} |
| RDPWrapOffsetFinder (llccd) | ${{ needs.build-offsetfinder.outputs.finder_ver }} |
> **Framework-dependent tools** (default) require **.NET 10 Desktop Runtime** on the target machine.
> Download: https://dotnet.microsoft.com/download/dotnet/10.0
> For environments without a runtime install, use the `*_sc.exe` files from `RDPWrapper-SelfContained.zip`.
### Downloads
| File | Contents | Use |
|---|---|---|
| `RDPWrapper.zip` | rdpwrap DLLs, C# tools (x64/x86/arm64), rdpWrapper GUI, rdpwrap.ini | **Main install package** — extract and run `RDPWInst_x64.exe -i -o` (or matching arch) |
| `RDPWrapper-SelfContained.zip` | Same C# tools bundled with .NET runtime (no separate runtime install needed) | Use when .NET 10 Desktop Runtime cannot be deployed |
| `RDPWrapper-<ver>-x64.msi` / `x86` / `arm64` | Windows Installer packages — GUI-friendly, self-registering uninstall | Preferred for managed/enterprise deployments |
| `RDPWrapOffsetFinder.zip` | RDPWrapOffsetFinder.exe + Zydis.dll for x64 and x86 | Generate offsets for an unknown termsrv.dll version manually |
### Merged pull requests
${{ steps.changelog.outputs.prs }}
### Individual loose files (used by the automated installer)
| File | Purpose |
|---|---|
| `rdpwrap.ini` | Offset database fetched automatically by the installer |
| `RDPWrapOffsetFinder_x64.exe` / `Zydis_x64.dll` | Loose x64 binaries downloaded by `TryAutoGenerateOffsets` |
| `RDPWrapOffsetFinder_x86.exe` / `Zydis_x86.dll` | Loose x86 binaries downloaded by `TryAutoGenerateOffsets` |
files: |
RDPWrapper.zip
RDPWrapper-SelfContained.zip
RDPWrapper-*-x64.msi
RDPWrapper-*-x86.msi
RDPWrapper-*-arm64.msi
RDPWrapOffsetFinder.zip
msi/rdpwrap.ini
msi/rdpwrap-arm-kb.ini
rdpwrap_x64.dll
rdpwrap_x86.dll
rdpwrap_arm64.dll
RDPWInst_x64.exe
RDPWInst_x86.exe
RDPWInst_arm64.exe
RDPConf_x64.exe
RDPConf_x86.exe
RDPConf_arm64.exe
RDPCheck_x64.exe
RDPCheck_x86.exe
RDPCheck_arm64.exe
RDPWrapOffsetFinder_x64.exe
RDPWrapOffsetFinder_x86.exe
Zydis_x64.dll
Zydis_x86.dll

@ -1,7 +1,14 @@
name: Build C++ DLL
name: Build C++ DLL
# Trigger on version tags (e.g. v1.7.0) or manually
# Trigger on:
# - PRs targeting main/master that touch C++ source (compile check before merge)
# - Version tag pushes (e.g. v1.7.0)
# - Manual dispatch
on:
pull_request:
branches: [main, master]
paths:
- 'src-x86-x64-Fusix/**'
push:
tags:
- 'v*'
@ -14,12 +21,18 @@ permissions:
contents: write
jobs:
# ── Build x64 and Win32 release DLLs ─────────────────────────────────────────
# ── Build x64 and Win32 release DLLs ─────────────────────────────────────────
build:
runs-on: windows-2022
strategy:
matrix:
platform: [x64, Win32]
include:
- platform: x64
msbuild_platform: x64
- platform: x86
msbuild_platform: Win32
- platform: arm64
msbuild_platform: ARM64
steps:
- name: Checkout repository
@ -36,7 +49,7 @@ jobs:
run: |
msbuild RDPWrap.vcxproj `
/p:Configuration=Release `
/p:Platform="${{ matrix.platform }}" `
/p:Platform="${{ matrix.msbuild_platform }}" `
/p:PlatformToolset=v143 `
/p:WindowsTargetPlatformVersion=10.0
@ -44,50 +57,9 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: rdpwrap-dll-${{ matrix.platform }}
path: src-x86-x64-Fusix/${{ matrix.platform }}/Release/rdpwrap.dll
path: src-x86-x64-Fusix/Release/${{ matrix.platform }}/rdpwrap.dll
if-no-files-found: error
# ── Assemble a GitHub Release from both DLL artifacts ────────────────────────
release:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Download x64 artifact
uses: actions/download-artifact@v4
with:
name: rdpwrap-dll-x64
path: ./dist/x64
- name: Download Win32 artifact
uses: actions/download-artifact@v4
with:
name: rdpwrap-dll-Win32
path: ./dist/x86
- name: Rename DLLs for release
run: |
mv dist/x64/rdpwrap.dll dist/rdpwrap_x64.dll
mv dist/x86/rdpwrap.dll dist/rdpwrap_x86.dll
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
prerelease: false
make_latest: false
body: |
## RDP Wrapper DLL ${{ github.ref_name }}
Built from `src-x86-x64-Fusix/` with MSVC v143 (VS 2022).
| File | Architecture |
|---|---|
| `rdpwrap_x64.dll` | x86-64 (64-bit Windows) |
| `rdpwrap_x86.dll` | x86 (32-bit Windows) |
> Place the appropriate DLL in `C:\Program Files\RDP Wrapper\rdpwrap.dll`
> and run `RDPWInst.exe -i` to install.
files: |
dist/rdpwrap_x64.dll
dist/rdpwrap_x86.dll
# NOTE: Full GitHub Releases (including rdpwrap DLLs) are published by build-and-release.yml.
# This workflow stops at the artifact upload step so that partial releases are never
# created on tag pushes. Run build-and-release.yml to produce the canonical release.

@ -0,0 +1,141 @@
name: Build C# Tools
# Trigger on:
# - PRs targeting main/master that touch C# source (compile check before merge)
# - Version tag pushes (e.g. v1.7.0)
# - Manual dispatch
# Produces self-contained, single-file RDPWInst / RDPConf / RDPCheck
# executables for x64 and x86.
on:
pull_request:
branches: [main, master]
paths:
- 'src-csharp/**'
push:
tags:
- 'v*'
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
permissions:
contents: write
jobs:
# ── Publish self-contained executables ────────────────────────────────────────
build:
runs-on: windows-2022
strategy:
matrix:
platform: [x64, x86, arm64]
include:
- platform: x64
rid: win-x64
- platform: x86
rid: win-x86
- platform: arm64
rid: win-arm64
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.x'
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/Directory.Build.props', '**/*.csproj') }}
restore-keys: |
nuget-${{ runner.os }}-
- name: Publish RDPWInst (${{ matrix.platform }})
run: |
dotnet publish src-csharp/RDPWInst/RDPWInst.csproj `
-c Release `
-r ${{ matrix.rid }} `
-p:Platform=${{ matrix.platform }} `
-p:PublishSingleFile=true `
-p:SelfContained=false `
--output dist/${{ matrix.platform }}/RDPWInst
- name: Publish RDPConf (${{ matrix.platform }})
run: |
dotnet publish src-csharp/RDPConf/RDPConf.csproj `
-c Release `
-r ${{ matrix.rid }} `
-p:Platform=${{ matrix.platform }} `
-p:PublishSingleFile=true `
-p:SelfContained=false `
--output dist/${{ matrix.platform }}/RDPConf
- name: Publish RDPCheck (${{ matrix.platform }})
run: |
dotnet publish src-csharp/RDPCheck/RDPCheck.csproj `
-c Release `
-r ${{ matrix.rid }} `
-p:Platform=${{ matrix.platform }} `
-p:PublishSingleFile=true `
-p:SelfContained=false `
--output dist/${{ matrix.platform }}/RDPCheck
# ── Optional: code-sign all three executables ─────────────────────────────
# Enable by setting repository variable USE_CERT_SIGNING=true and adding
# secrets CODESIGN_CERT_BASE64 (PFX as base64) and CODESIGN_CERT_PASSWORD.
- name: Sign executables (${{ matrix.platform }})
if: vars.USE_CERT_SIGNING == 'true'
env:
CODESIGN_CERT_BASE64: ${{ secrets.CODESIGN_CERT_BASE64 }}
CODESIGN_CERT_PASSWORD: ${{ secrets.CODESIGN_CERT_PASSWORD }}
shell: pwsh
run: |
$pfxPath = "$env:RUNNER_TEMP\codesign.pfx"
[IO.File]::WriteAllBytes($pfxPath,
[Convert]::FromBase64String($env:CODESIGN_CERT_BASE64))
$exes = @(
"dist\${{ matrix.platform }}\RDPWInst\RDPWInst.exe",
"dist\${{ matrix.platform }}\RDPConf\RDPConf.exe",
"dist\${{ matrix.platform }}\RDPCheck\RDPCheck.exe"
)
$signtool = (Resolve-Path "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" |
Sort-Object | Select-Object -Last 1).Path
foreach ($exe in $exes) {
& $signtool sign `
/fd SHA256 `
/f $pfxPath `
/p $env:CODESIGN_CERT_PASSWORD `
/tr http://timestamp.digicert.com `
/td SHA256 `
$exe
}
Remove-Item $pfxPath -Force
- name: Collect artifacts (${{ matrix.platform }})
run: |
Copy-Item dist/${{ matrix.platform }}/RDPWInst/RDPWInst.exe RDPWInst_${{ matrix.platform }}.exe
Copy-Item dist/${{ matrix.platform }}/RDPConf/RDPConf.exe RDPConf_${{ matrix.platform }}.exe
Copy-Item dist/${{ matrix.platform }}/RDPCheck/RDPCheck.exe RDPCheck_${{ matrix.platform }}.exe
- name: Upload ${{ matrix.platform }} artifacts
uses: actions/upload-artifact@v4
with:
name: csharp-tools-${{ matrix.platform }}
path: |
RDPWInst_${{ matrix.platform }}.exe
RDPConf_${{ matrix.platform }}.exe
RDPCheck_${{ matrix.platform }}.exe
if-no-files-found: error
# NOTE: Full GitHub Releases (including C# tools) are published by publish-ini.yml.
# This workflow intentionally stops at the artifact upload step so that partial
# releases are never created on the tag. Run publish-ini.yml manually or let it
# fire from an INI push to produce the canonical release.

@ -0,0 +1,73 @@
name: Build MSI Check
# Lightweight PR check that validates msi/RDPWInst.wxs compiles cleanly.
# We stub the binary inputs (DLL/EXE) with placeholder files so WiX can
# validate the package structure without needing a full C++ / C# build.
# For the real release build with actual binaries, see build-and-release.yml.
on:
pull_request:
branches: [main, master]
paths:
- 'msi/**'
- '.github/workflows/build-msi-check.yml'
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
wix-check:
runs-on: windows-2022
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.x'
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/Directory.Build.props', '**/*.csproj', '**/global.json') }}
restore-keys: |
nuget-${{ runner.os }}-
# Create minimal placeholder files so WiX can resolve all File references
# in RDPWInst.wxs without requiring an actual C++ or C# build.
# WiX v5 harvests file size at build time but does not validate binary content.
- name: Create placeholder binary inputs
shell: pwsh
run: |
$arch = 'x64'
# Placeholder DLL and EXEs — WiX only needs the files to exist
foreach ($name in @("rdpwrap_$arch.dll", "RDPWInst_$arch.exe", "RDPConf_$arch.exe", "RDPCheck_$arch.exe")) {
$dest = "msi\$name"
if (-not (Test-Path $dest)) {
[System.IO.File]::WriteAllBytes($dest, [byte[]](0x4D, 0x5A)) # MZ stub
Write-Host "Created stub: $dest"
}
}
Write-Host "Placeholder files in msi\:"
Get-ChildItem msi\ -File | Format-Table Name, Length
# Build only the x64 MSI — enough to validate WXS structure and WiX rules.
# A failure here means a real WiX schema/syntax/reference error that would
# also break the release build.
- name: Build MSI (x64 WiX check)
shell: pwsh
run: |
dotnet build msi/RDPWInst.wixproj `
-c Release `
/p:Platform=x64 `
/p:PackageVersion=0.0.1 `
/p:OutputPath="$PWD/msi_check_out/x64"
if ($LASTEXITCODE -ne 0) { throw "WiX build failed — review errors above" }
Write-Host "WiX check passed."
Get-ChildItem msi_check_out -Recurse -Filter "*.msi" | Format-Table FullName, Length

@ -0,0 +1,93 @@
name: Build RDPWrapOffsetFinder
# Trigger on:
# - PRs targeting main/master that touch the OffsetFinder submodule or Zydis
# - Version tag pushes (e.g. v1.7.0)
# - Manual dispatch
on:
pull_request:
branches: [main, master]
paths:
- 'src-csharp/RDPOffsetFinder/**'
push:
tags:
- 'v*'
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: write
jobs:
# ── Build x64 and Win32 release exes ─────────────────────────────────────────
build:
runs-on: windows-2022
strategy:
matrix:
include:
- platform: x64
zydis_cfg: "Release MD DLL"
zydis_bin: ReleaseX64
finder_plat: x64
- platform: Win32
zydis_cfg: "Release MD DLL"
zydis_bin: ReleaseX86
finder_plat: Win32
steps:
- name: Checkout repository (with submodules)
uses: actions/checkout@v6
with:
submodules: recursive
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v3
# Build Zydis as a shared DLL so we can ship Zydis.dll alongside the exe
- name: Build Zydis (${{ matrix.platform }})
working-directory: src-csharp/RDPOffsetFinder
run: |
msbuild zydis\msvc\zydis\Zydis.vcxproj `
/p:Configuration="${{ matrix.zydis_cfg }}" `
/p:Platform="${{ matrix.platform }}" `
/p:PlatformToolset=v143 `
/v:minimal
# Build the offset finder (links against the Zydis import lib just produced)
- name: Build RDPWrapOffsetFinder (${{ matrix.platform }})
working-directory: src-csharp/RDPOffsetFinder
run: |
msbuild RDPWrapOffsetFinder\RDPWrapOffsetFinder.vcxproj `
/p:Configuration=Release `
/p:Platform="${{ matrix.platform }}" `
/p:PlatformToolset=v143 `
/v:minimal
- name: Collect outputs
shell: pwsh
run: |
$arch = if ("${{ matrix.platform }}" -eq "Win32") { "x86" } else { "x64" }
$root = "src-csharp/RDPOffsetFinder"
$exe = "$root/RDPWrapOffsetFinder/${{ matrix.finder_plat }}/Release/RDPWrapOffsetFinder.exe"
$dll = "$root/zydis/msvc/bin/${{ matrix.zydis_bin }}/Zydis.dll"
Write-Host "Exe: $(Get-Item $exe | Select-Object -Exp Length) bytes"
Write-Host "Dll: $(Get-Item $dll | Select-Object -Exp Length) bytes"
Copy-Item $exe ".\RDPWrapOffsetFinder_$arch.exe"
Copy-Item $dll ".\Zydis_$arch.dll"
- name: Upload ${{ matrix.platform }} artifact
uses: actions/upload-artifact@v4
with:
name: finder-${{ matrix.platform }}
path: |
RDPWrapOffsetFinder_*.exe
Zydis_*.dll
if-no-files-found: error
# NOTE: Full GitHub Releases (including OffsetFinder binaries) are published by build-and-release.yml.
# This workflow stops at the artifact upload step so that partial releases are never
# created on tag pushes. Run build-and-release.yml to produce the canonical release.

@ -1,188 +0,0 @@
name: Publish INI and Offset Finder Tools
on:
push:
branches: [main, master]
paths:
- 'res/rdpwrap.ini'
- 'tools/RDPWrapOffsetFinder/**'
- '.github/workflows/publish-ini.yml'
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: write
jobs:
publish:
runs-on: windows-2022
steps:
- name: Checkout repository
uses: actions/checkout@v6
# ── Build rdpwrap.dll for both x64 and Win32 ────────────────────────────
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v3
- name: Build rdpwrap.dll (x64 and Win32)
shell: pwsh
run: |
foreach ($platform in @('x64', 'Win32')) {
msbuild src-x86-x64-Fusix/RDPWrap.vcxproj `
/p:Configuration=Release `
/p:Platform="$platform" `
/p:PlatformToolset=v143 `
/p:WindowsTargetPlatformVersion=10.0 `
/v:minimal
}
Copy-Item src-x86-x64-Fusix/x64/Release/rdpwrap.dll .\rdpwrap_x64.dll
Copy-Item src-x86-x64-Fusix/Win32/Release/rdpwrap.dll .\rdpwrap_x86.dll
Write-Host "Built DLLs:"
Get-Item .\rdpwrap_x64.dll, .\rdpwrap_x86.dll | Format-Table Name, Length
# ── Extract the date string used for tagging and the updated field ──────
- name: Get release metadata
id: meta
shell: pwsh
run: |
$date = Get-Date -Format "yyyy.MM.dd.HHmm"
$stamp = Get-Date -Format "yyyy-MM-dd HH:mm UTC"
# Pull the Updated= line out of the INI to surface in release notes
$iniContent = Get-Content res/rdpwrap.ini -Raw
$iniDate = if ($iniContent -match 'Updated=([^\r\n]+)') { $Matches[1] } else { 'unknown' }
echo "date=$date" >> $env:GITHUB_OUTPUT
echo "stamp=$stamp" >> $env:GITHUB_OUTPUT
echo "inidate=$iniDate" >> $env:GITHUB_OUTPUT
# ── Stage RDPWrapOffsetFinder from committed tools/ ──────────────────────
- name: Stage RDPWrapOffsetFinder from tools/
id: finder
shell: pwsh
run: |
$version = (Get-Content tools/RDPWrapOffsetFinder/VERSION -Raw).Trim()
Copy-Item tools/RDPWrapOffsetFinder/x64/RDPWrapOffsetFinder.exe .\RDPWrapOffsetFinder_x64.exe
Copy-Item tools/RDPWrapOffsetFinder/x64/Zydis.dll .\Zydis_x64.dll
Copy-Item tools/RDPWrapOffsetFinder/x86/RDPWrapOffsetFinder.exe .\RDPWrapOffsetFinder_x86.exe
Copy-Item tools/RDPWrapOffsetFinder/x86/Zydis.dll .\Zydis_x86.dll
Write-Host "Staged finder tools (version $version):"
Get-Item .\RDPWrapOffsetFinder_*.exe, .\Zydis_*.dll | Format-Table Name, Length
echo "finder_ver=$version" >> $env:GITHUB_OUTPUT
# ── Download the latest rdpWrapper GUI app from sergiye ─────────────────
- name: Download rdpWrapper (sergiye)
id: wrapper
shell: pwsh
run: |
$apiUrl = "https://api.github.com/repos/sergiye/rdpWrapper/releases/latest"
$release = Invoke-RestMethod -Uri $apiUrl -Headers @{ "User-Agent" = "rdpwrap-ci" }
foreach ($arch in @('x64', 'x86')) {
$asset = $release.assets |
Where-Object { $_.name -eq "rdpWrapper_$arch.exe" } |
Select-Object -First 1
if (-not $asset) {
throw "Could not find rdpWrapper_$arch.exe in sergiye release $($release.tag_name)"
}
Write-Host "Downloading $($asset.browser_download_url)"
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile ".\rdpWrapper_$arch.exe"
}
Write-Host "Wrapper exes:"
Get-Item .\rdpWrapper_x64.exe, .\rdpWrapper_x86.exe | Format-Table Name, Length
echo "wrapper_ver=$($release.tag_name)" >> $env:GITHUB_OUTPUT
# ── Validate the INI has required sections ───────────────────────────────
- name: Validate INI
shell: pwsh
run: |
$ini = Get-Content res/rdpwrap.ini -Raw
foreach ($section in @('[Main]', '[SLPolicy]', '[PatchCodes]')) {
if ($ini -notmatch [regex]::Escape($section)) {
throw "INI validation failed: missing required section $section"
}
}
$sectionCount = ([regex]::Matches($ini, '^\[[\d\.]+\]', 'Multiline')).Count
Write-Host "INI is valid. Windows-version sections: $sectionCount"
# ── Assemble the two user-facing distribution bundles ────────────────────
- name: Create distribution bundles
shell: pwsh
run: |
# RDPWrapper.zip - complete install package
$d = ".\bundle_wrapper"
New-Item -ItemType Directory -Path $d -Force | Out-Null
Copy-Item .\rdpwrap_x64.dll "$d\rdpwrap_x64.dll"
Copy-Item .\rdpwrap_x86.dll "$d\rdpwrap_x86.dll"
Copy-Item .\rdpWrapper_x64.exe "$d\rdpWrapper_x64.exe"
Copy-Item .\rdpWrapper_x86.exe "$d\rdpWrapper_x86.exe"
Copy-Item res\rdpwrap.ini "$d\rdpwrap.ini"
Copy-Item bin\install.bat "$d\install.bat"
Copy-Item bin\uninstall.bat "$d\uninstall.bat"
Copy-Item bin\update.bat "$d\update.bat"
Compress-Archive -Path "$d\*" -DestinationPath ".\RDPWrapper.zip" -Force
Remove-Item $d -Recurse -Force
# RDPWrapOffsetFinder.zip - offset finder with per-arch subfolders
$f = ".\bundle_finder"
foreach ($arch in @('x64', 'x86')) {
$dir = "$f\$arch"
New-Item -ItemType Directory -Path $dir -Force | Out-Null
Copy-Item ".\RDPWrapOffsetFinder_$arch.exe" "$dir\RDPWrapOffsetFinder.exe"
Copy-Item ".\Zydis_$arch.dll" "$dir\Zydis.dll"
}
Compress-Archive -Path "$f\*" -DestinationPath ".\RDPWrapOffsetFinder.zip" -Force
Remove-Item $f -Recurse -Force
Write-Host "Distribution bundles:"
Get-Item .\RDPWrapper.zip, .\RDPWrapOffsetFinder.zip | Format-Table Name, Length
# ── Create/update a versioned GitHub Release with all assets ─────────────
- name: Publish release
uses: softprops/action-gh-release@v2
with:
tag_name: "ini-${{ steps.meta.outputs.date }}"
name: "INI Update ${{ steps.meta.outputs.date }}"
prerelease: false
make_latest: true
body: |
## RDP Wrapper - Automated Release
| Field | Value |
|---|---|
| INI `Updated` date | `${{ steps.meta.outputs.inidate }}` |
| Published | ${{ steps.meta.outputs.stamp }} |
| rdpWrapper (sergiye) | ${{ steps.wrapper.outputs.wrapper_ver }} |
| RDPWrapOffsetFinder (llccd) | ${{ steps.finder.outputs.finder_ver }} |
### Downloads
| File | Contents | Use |
|---|---|---|
| `RDPWrapper.zip` | rdpwrap_x64.dll, rdpwrap_x86.dll, rdpWrapper_x64.exe, rdpWrapper_x86.exe, rdpwrap.ini, install/uninstall/update.bat | Main install package - extract and run `rdpWrapper_x64.exe` |
| `RDPWrapOffsetFinder.zip` | x64\RDPWrapOffsetFinder.exe + Zydis.dll, x86\RDPWrapOffsetFinder.exe + Zydis.dll | Generate offsets for an unknown termsrv.dll version manually |
### Individual files (used by the automated installer)
| File | Purpose |
|---|---|
| `rdpwrap.ini` | Offset database fetched automatically by the installer |
| `RDPWrapOffsetFinder_x64.exe` / `Zydis_x64.dll` | Loose x64 binaries downloaded by `TryAutoGenerateOffsets` |
| `RDPWrapOffsetFinder_x86.exe` / `Zydis_x86.dll` | Loose x86 binaries downloaded by `TryAutoGenerateOffsets` |
files: |
RDPWrapper.zip
RDPWrapOffsetFinder.zip
res/rdpwrap.ini
rdpwrap_x64.dll
rdpwrap_x86.dll
RDPWrapOffsetFinder_x64.exe
RDPWrapOffsetFinder_x86.exe
Zydis_x64.dll
Zydis_x86.dll

@ -1,120 +0,0 @@
name: Update RDPWrapOffsetFinder tools
# Run manually to pull a new version of llccd/RDPWrapOffsetFinder into tools/.
# The workflow commits the updated binaries and opens a pull request.
on:
workflow_dispatch:
inputs:
tag:
description: "llccd release tag to fetch (leave blank for latest)"
required: false
default: ""
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: write
pull-requests: write
jobs:
update:
runs-on: windows-2022
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Download and extract RDPWrapOffsetFinder
id: fetch
shell: pwsh
run: |
$tag = "${{ github.event.inputs.tag }}"
if ($tag -eq "") {
$apiUrl = "https://api.github.com/repos/llccd/RDPWrapOffsetFinder/releases/latest"
} else {
$apiUrl = "https://api.github.com/repos/llccd/RDPWrapOffsetFinder/releases/tags/$tag"
}
$release = Invoke-RestMethod -Uri $apiUrl -Headers @{ "User-Agent" = "rdpwrap-ci" }
$version = $release.tag_name
Write-Host "Fetching version $version"
$asset = $release.assets | Where-Object { $_.name -like "*.zip" } | Select-Object -First 1
if (-not $asset) { throw "No zip asset found in release $version" }
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile finder.zip -UseBasicParsing
Expand-Archive -Path finder.zip -DestinationPath .\finder -Force
Write-Host "Zip contents:"
Get-ChildItem -Recurse .\finder | Select-Object FullName, Length
# The llccd zip uses "64bit/" and "32bit/" subfolder names.
# We pick the symbol-enabled exe (not the _nosymbol variant) from each arch folder.
function Get-Bin($filter, $archHint) {
$hits = Get-ChildItem -Recurse .\finder -Filter $filter |
Where-Object { $_.FullName -match $archHint } |
Where-Object { $_.Name -notmatch "nosymbol" }
return ($hits | Select-Object -First 1)
}
$x64exe = Get-Bin "RDPWrapOffsetFinder*.exe" "64bit"
$x64dll = Get-Bin "Zydis*.dll" "64bit"
$x86exe = Get-Bin "RDPWrapOffsetFinder*.exe" "32bit"
$x86dll = Get-Bin "Zydis*.dll" "32bit"
# Fall back to x64 if the release only ships one arch
if (-not $x86exe) { $x86exe = $x64exe }
if (-not $x86dll) { $x86dll = $x64dll }
if (-not $x64exe) { throw "Could not locate x64 exe" }
if (-not $x64dll) { throw "Could not locate x64 Zydis.dll" }
New-Item -ItemType Directory -Path tools\RDPWrapOffsetFinder\x64 -Force | Out-Null
New-Item -ItemType Directory -Path tools\RDPWrapOffsetFinder\x86 -Force | Out-Null
Copy-Item $x64exe.FullName tools\RDPWrapOffsetFinder\x64\RDPWrapOffsetFinder.exe -Force
Copy-Item $x64dll.FullName tools\RDPWrapOffsetFinder\x64\Zydis.dll -Force
Copy-Item $x86exe.FullName tools\RDPWrapOffsetFinder\x86\RDPWrapOffsetFinder.exe -Force
Copy-Item $x86dll.FullName tools\RDPWrapOffsetFinder\x86\Zydis.dll -Force
Set-Content -Path tools\RDPWrapOffsetFinder\VERSION -Value $version
Write-Host "Staged tools:"
Get-ChildItem -Recurse tools\RDPWrapOffsetFinder | Select-Object FullName, Length
echo "version=$version" >> $env:GITHUB_OUTPUT
- name: Commit and push to a new branch
id: push
shell: pwsh
run: |
$version = "${{ steps.fetch.outputs.version }}"
$branch = "chore/update-finder-tools-$version"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b $branch
git add tools/RDPWrapOffsetFinder
if ((git diff --cached --name-only) -eq "") {
Write-Host "No changes - tools already up to date"
exit 0
}
git commit -m "chore: update RDPWrapOffsetFinder tools to $version"
git push origin $branch
echo "branch=$branch" >> $env:GITHUB_OUTPUT
- name: Create pull request
if: steps.push.outputs.branch != ''
shell: pwsh
run: |
$version = "${{ steps.fetch.outputs.version }}"
$branch = "${{ steps.push.outputs.branch }}"
gh pr create `
--title "chore: update RDPWrapOffsetFinder tools to $version" `
--body "Automated update of the committed RDPWrapOffsetFinder binaries in ``tools/RDPWrapOffsetFinder/`` to upstream release $version.
Triggered manually via the **Update RDPWrapOffsetFinder tools** workflow.
Merge to include the updated binaries in the next INI release." `
--base master `
--head $branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

24
.gitignore vendored

@ -6,6 +6,9 @@
__history/
*.~*
# Local build output (produced by local builds; not committed)
build/
# Compiled binaries
*.dcu
*.exe
@ -15,3 +18,24 @@ __history/
# MSI local stuff
*.wixobj
*.wixpdb
msi/bin/
msi/obj/
msi_out/
# Staged CI inputs inside msi/ (DLLs and EXEs copied there before WiX runs;
# generated at build time — not source files)
msi/*.dll
msi/*.exe
# C# build outputs
src-csharp/**/bin/
src-csharp/**/obj/
# Staged resource: copied from msi/rdpwrap.ini at build time, not source
src-csharp/RDPWInst/Resources/rdpwrap.ini
src-csharp/RDPWInst/Resources/rdpw32.dll
src-csharp/RDPWInst/Resources/rdpw64.dll
src-csharp/RDPWInst/Resources/license.txt
# C++ build outputs
src-x86-x64-Fusix/Release/
src-x86-x64-Fusix/obj/

4
.gitmodules vendored

@ -0,0 +1,4 @@
[submodule "src-csharp/RDPOffsetFinder"]
path = src-csharp/RDPOffsetFinder
url = https://github.com/llccd/RDPWrapOffsetFinder
shallow = true

@ -2,11 +2,12 @@
> **Maintained fork** by [@sjackson0109](https://github.com/sjackson0109) — based on the original work by [Stas'M / binarymaster](https://github.com/stascorp/rdpwrap).
[![Telegram](https://img.shields.io/badge/chat-Telegram-blue.svg)](https://t.me/rdpwrap)
![Environment](https://img.shields.io/badge/Windows-Vista%20through%2011-brightgreen.svg)
[![Release](https://img.shields.io/github/release/sjackson0109/rdpwrap.svg)](https://github.com/sjackson0109/rdpwrap/releases)
[![INI publish](https://github.com/sjackson0109/rdpwrap/actions/workflows/publish-ini.yml/badge.svg)](https://github.com/sjackson0109/rdpwrap/actions/workflows/publish-ini.yml)
[![Build and Release](https://github.com/sjackson0109/rdpwrap/actions/workflows/build-and-release.yml/badge.svg)](https://github.com/sjackson0109/rdpwrap/actions/workflows/build-and-release.yml)
[![Build C++ DLL](https://github.com/sjackson0109/rdpwrap/actions/workflows/build-cpp.yml/badge.svg)](https://github.com/sjackson0109/rdpwrap/actions/workflows/build-cpp.yml)
[![Build C# tools](https://github.com/sjackson0109/rdpwrap/actions/workflows/build-csharp.yml/badge.svg)](https://github.com/sjackson0109/rdpwrap/actions/workflows/build-csharp.yml)
[![Build OffsetFinder](https://github.com/sjackson0109/rdpwrap/actions/workflows/build-offsetfinder.yml/badge.svg)](https://github.com/sjackson0109/rdpwrap/actions/workflows/build-offsetfinder.yml)
![License](https://img.shields.io/github/license/sjackson0109/rdpwrap.svg)
![Downloads](https://img.shields.io/github/downloads/sjackson0109/rdpwrap/latest/total.svg)
![TotalDownloads](https://img.shields.io/github/downloads/sjackson0109/rdpwrap/total.svg)
@ -15,41 +16,22 @@ The goal of this project is to enable Remote Desktop Host support and concurrent
RDP Wrapper works as a layer between Service Control Manager and Terminal Services, so the original termsrv.dll file remains untouched. Also this method is very strong against Windows Update.
[pVistaST]: http://stascorp.com/images/rdpwrap/pVistaST.jpg
[pVistaHB]: http://stascorp.com/images/rdpwrap/pVistaHB.jpg
[pWin7ST]: http://stascorp.com/images/rdpwrap/pWin7ST.jpg
[pWin7HB]: http://stascorp.com/images/rdpwrap/pWin7HB.jpg
[pWin8DP]: http://stascorp.com/images/rdpwrap/pWin8DP.jpg
[pWin8CP]: http://stascorp.com/images/rdpwrap/pWin8CP.jpg
[pWin8RP]: http://stascorp.com/images/rdpwrap/pWin8RP.jpg
[pWin8]: http://stascorp.com/images/rdpwrap/pWin8.jpg
[pWin81P]: http://stascorp.com/images/rdpwrap/pWin81P.jpg
[pWin81]: http://stascorp.com/images/rdpwrap/pWin81.jpg
[pWin10TP]: http://stascorp.com/images/rdpwrap/pWin10TP.jpg
[pWin10PTP]: http://stascorp.com/images/rdpwrap/pWin10PTP.jpg
[pWin10]: http://stascorp.com/images/rdpwrap/pWin10.jpg
[fVistaST]: http://stascorp.com/images/rdpwrap/VistaST.png
[fVistaHB]: http://stascorp.com/images/rdpwrap/VistaHB.png
[fWin7ST]: http://stascorp.com/images/rdpwrap/Win7ST.png
[fWin7HB]: http://stascorp.com/images/rdpwrap/Win7HB.png
[fWin8DP]: http://stascorp.com/images/rdpwrap/Win8DP.png
[fWin8CP]: http://stascorp.com/images/rdpwrap/Win8CP.png
[fWin8RP]: http://stascorp.com/images/rdpwrap/Win8RP.png
[fWin8]: http://stascorp.com/images/rdpwrap/Win8.png
[fWin81P]: http://stascorp.com/images/rdpwrap/Win81P.png
[fWin81]: http://stascorp.com/images/rdpwrap/Win81.png
[fWin10TP]: http://stascorp.com/images/rdpwrap/Win10TP.png
[fWin10PTP]: http://stascorp.com/images/rdpwrap/Win10PTP.png
[fWin10]: http://stascorp.com/images/rdpwrap/Win10.png
| NT Version | Screenshots |
| ------------- | ----------- |
| Windows Vista | [![Windows Vista Starter][pVistaST]][fVistaST] [![Windows Vista Home Basic][pVistaHB]][fVistaHB] |
| Windows 7 | [![Windows 7 Starter][pWin7ST]][fWin7ST] [![Windows 7 Home Basic][pWin7HB]][fWin7HB] |
| Windows 8 | [![Windows 8 Developer Preview][pWin8DP]][fWin8DP] [![Windows 8 Consumer Preview][pWin8CP]][fWin8CP] [![Windows 8 Release Preview][pWin8RP]][fWin8RP] [![Windows 8][pWin8]][fWin8] |
| Windows 8.1 | [![Windows 8.1 Preview][pWin81P]][fWin81P] [![Windows 8.1][pWin81]][fWin81] |
| Windows 10 | [![Windows 10 Technical Preview][pWin10TP]][fWin10TP] [![Windows 10 Pro Technical Preview][pWin10PTP]][fWin10PTP] [![Windows 10][pWin10]][fWin10] |
> **Historical screenshots** (Vista / 7 / 8 / 10 from the original Stas'M project) are archived at:
> https://web.archive.org/web/2015*/http://stascorp.com/images/rdpwrap/*
### Screenshots
> Screenshots are captured on Windows 11 after a successful install.
> Source files live in [`docs/images/`](docs/images/) — see the [capture guide](docs/images/README.md) if you want to contribute updated screenshots.
| RDPConf — configuration | RDPCheck — supported | RDPCheck — warning |
|:---:|:---:|:---:|
| ![RDPConf configuration window](docs/images/RDPWrapperConfig.png) | ![RDPCheck showing Supported](docs/images/RDPWrapperCheck.png) | ![RDPCheck showing Warning](docs/images/RDPWrapperCheckWarning.png) |
| MSI Installer — welcome | MSI Installer — complete |
|:---:|:---:|
| ![MSI installer welcome screen](docs/images/RDPWrapperMSI1.png) | ![MSI installer completion screen](docs/images/RDPWrapperMSI2.png) |
---
[WinPPE]: http://forums.mydigitallife.info/threads/39411-Windows-Product-Policy-Editor
@ -82,16 +64,72 @@ It's recommended to have original termsrv.dll file with the RDP Wrapper installa
- **ARM** for Windows RT (see links below)
- **IA-64** for Itanium-based Windows Server? *Well, I have no idea* :)
### Repository structure:
```
├── msi/ # WiX v5 MSI project + INI offset databases (rdpwrap.ini, rdpwrap-arm-kb.ini)
├── docs/ # Developer documentation
├── src-x86-x64-Fusix/ # C++ — core rdpwrap.dll (native Win32, MSVC)
└── src-csharp/ # C# .NET 10 — all management-plane tools
├── Directory.Build.props # Shared build settings (framework, author metadata)
├── RDPWrap.sln # Visual Studio solution
├── RDPWrap/ # Shared helper library (P/Invoke, registry, service helpers)
├── RDPWInst/ # Command-line installer / uninstaller
├── RDPConf/ # WinForms configuration GUI
├── RDPCheck/ # WinForms RDP loopback tester
└── RDPOffsetFinder/ # Git submodule — llccd/RDPWrapOffsetFinder (C++)
└── zydis/ # Git submodule — zyantific/zydis disassembler
```
### Building the binaries:
- **x86 Delphi version** (installer, config tool, checker) — requires *Embarcadero RAD Studio 2010* or later; no automated CI yet
- **x86/x64 C++ version** (`rdpwrap.dll`) — can be built locally with *Visual Studio 2013+*, or automatically via the [Build C++ DLL](.github/workflows/build-cpp.yml) GitHub Actions workflow (uses MSVC v143 / VS 2022) by pushing a `v*` tag
#### Prerequisites
- **.NET SDK 10** — all C# tools (`src-csharp/`)
- Download: https://dotnet.microsoft.com/download/dotnet/10.0
- **Visual Studio 2022 / MSVC Build Tools v143** — two C++ components:
- `src-x86-x64-Fusix/` — core `rdpwrap.dll`
- `src-csharp/RDPOffsetFinder/` — offset finder (pulled via git submodule)
#### Clone with submodules
```powershell
git clone --recurse-submodules https://github.com/sjackson0109/rdpwrap.git
# or if already cloned:
git submodule update --init --recursive
```
#### Build C# tools locally
```powershell
# Debug build (both platforms)
dotnet build src-csharp/RDPWrap.sln -p:Platform=x64
dotnet build src-csharp/RDPWrap.sln -p:Platform=x86
# Release publish — single-file EXE (requires .NET 10 Desktop Runtime on target)
dotnet publish src-csharp/RDPWInst/RDPWInst.csproj -c Release -r win-x64 -p:PublishSingleFile=true -p:SelfContained=false
dotnet publish src-csharp/RDPConf/RDPConf.csproj -c Release -r win-x64 -p:PublishSingleFile=true -p:SelfContained=false
dotnet publish src-csharp/RDPCheck/RDPCheck.csproj -c Release -r win-x64 -p:PublishSingleFile=true -p:SelfContained=false
```
#### Build `rdpwrap.dll` locally
```powershell
msbuild src-x86-x64-Fusix/RDPWrap.vcxproj /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143
```
#### Build `RDPWrapOffsetFinder` locally
```powershell
# Build Zydis DLL first
msbuild src-csharp/RDPOffsetFinder/zydis/msvc/zydis/Zydis.vcxproj /p:Configuration="Release MD DLL" /p:Platform=x64 /p:PlatformToolset=v143
# Then build the offset finder
msbuild src-csharp/RDPOffsetFinder/RDPWrapOffsetFinder/RDPWrapOffsetFinder.vcxproj /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143
```
### CI/CD Pipelines:
| Workflow | Trigger | Output |
|---|---|---|
| [publish-ini.yml](.github/workflows/publish-ini.yml) | Push to `main`/`master` touching `res/rdpwrap.ini`, or manual | GitHub Release with `rdpwrap.ini`, `RDPWrapOffsetFinder_x64/x86.exe`, `Zydis_x64/x86.dll` |
| [build-cpp.yml](.github/workflows/build-cpp.yml) | Version tag push (`v*`) | GitHub Release with `rdpwrap_x64.dll` and `rdpwrap_x86.dll` |
| [build-and-release.yml](.github/workflows/build-and-release.yml) | Push to `main`/`master` touching any source, INI, or the workflow file itself; or manual | **Canonical GitHub Release** — DLLs (x64/x86/arm64), C# tools, MSI packages, self-contained bundles, OffsetFinder, `rdpwrap.ini`, `rdpwrap-arm-kb.ini` |
| [build-csharp.yml](.github/workflows/build-csharp.yml) | PR to `main`/`master` touching `src-csharp/**`; version tag push (`v*`); or manual | PR compile check + artifact upload (no release) — use `build-and-release.yml` for a full release |
| [build-cpp.yml](.github/workflows/build-cpp.yml) | PR to `main`/`master` touching `src-x86-x64-Fusix/**`; version tag push (`v*`); or manual | PR compile check + artifact upload (no release) — use `build-and-release.yml` for a full release |
| [build-offsetfinder.yml](.github/workflows/build-offsetfinder.yml) | PR to `main`/`master` touching `src-csharp/RDPOffsetFinder/**`; version tag push (`v*`); or manual | PR compile check + artifact upload (no release) — use `build-and-release.yml` for a full release |
[andrewblock]: http://web.archive.org/web/20150810054558/http://andrewblock.net/enable-remote-desktop-on-windows-8-core/
[mydigitallife]: http://forums.mydigitallife.info/threads/55935-RDP-Wrapper-Library-(works-with-Windows-8-1-Basic)
@ -125,14 +163,17 @@ It's recommended to have original termsrv.dll file with the RDP Wrapper installa
### Files in release package:
| File name | Description |
| --------- | ----------- |
| `RDPWInst.exe` | RDP Wrapper Library installer/uninstaller |
| `RDPCheck.exe` | Local RDP Checker (you can check the RDP is working) |
| `RDPConf.exe` | RDP Wrapper Configuration |
| `install.bat` | Quick install batch file |
| `uninstall.bat` | Quick uninstall batch file |
| `update.bat` | Quick update batch file |
| File name | Architecture | Description |
| --------- | ------------ | ----------- |
| `RDPWInst_x64.exe` | x64 | RDP Wrapper Library installer/uninstaller (C#, requires .NET 10) |
| `RDPWInst_x86.exe` | x86 | RDP Wrapper Library installer/uninstaller (C#, requires .NET 10) |
| `RDPCheck_x64.exe` | x64 | Local RDP Checker — verify RDP is working (C#, requires .NET 10) |
| `RDPCheck_x86.exe` | x86 | Local RDP Checker — verify RDP is working (C#, requires .NET 10) |
| `RDPConf_x64.exe` | x64 | RDP Wrapper Configuration GUI (C#, requires .NET 10) |
| `RDPConf_x86.exe` | x86 | RDP Wrapper Configuration GUI (C#, requires .NET 10) |
| `rdpwrap_x64.dll` | x64 | Core RDP Wrapper DLL (C++, no runtime required) |
| `rdpwrap_x86.dll` | x86 | Core RDP Wrapper DLL (C++, no runtime required) |
| `rdpwrap.ini` | — | Offset database (updated automatically on every INI push) |
### Frequently Asked Questions
@ -146,11 +187,11 @@ There is no definitive answer, see [this discussion](https://github.com/stascorp
> The installer tries to access the Internet, is it normal behaviour?
Yes, it works in online mode by default. You may disable it by removing `-o` flag in the `install.bat` file.
Yes, it works in online mode by default. You may disable it by passing `-i` without the `-o` flag: `RDPWInst_x64.exe -i`.
> What is online install mode?
Online install mode was introduced in version 1.6.1. When installing for the first time using this mode, the installer downloads the [latest `rdpwrap.ini`](https://github.com/sjackson0109/rdpwrap/releases/latest/download/rdpwrap.ini) from this repository's GitHub Releases — published automatically by CI/CD whenever `res/rdpwrap.ini` is updated. If your `termsrv.dll` version is not yet listed in the downloaded INI, the installer will additionally download [RDPWrapOffsetFinder](https://github.com/llccd/RDPWrapOffsetFinder) and attempt to auto-generate the missing offsets on the spot.
Online install mode was introduced in version 1.6.1. When installing for the first time using this mode, the installer downloads the [latest `rdpwrap.ini`](https://github.com/sjackson0109/rdpwrap/releases/latest/download/rdpwrap.ini) from this repository's GitHub Releases — published automatically by CI/CD whenever `msi/rdpwrap.ini` is updated. If your `termsrv.dll` version is not yet listed in the downloaded INI, the installer will additionally download [RDPWrapOffsetFinder](https://github.com/llccd/RDPWrapOffsetFinder) and attempt to auto-generate the missing offsets on the spot.
> What is INI file and why we need it?
@ -162,7 +203,7 @@ Beginning with version 1.5 the `rdpwrap.dll` is not updated anymore, since all s
> Config Tool shows `[not supported]` and RDP doesn't work. What can I do?
Make sure you're connected to the Internet and run `update.bat`. This will download the latest INI from GitHub Releases and, if your `termsrv.dll` version is still missing, will automatically run [RDPWrapOffsetFinder](https://github.com/llccd/RDPWrapOffsetFinder) to generate offsets for your specific build.
Make sure you're connected to the Internet and run `RDPWInst_x64.exe -w` from an Administrator command prompt. This will download the latest INI from GitHub Releases and, if your `termsrv.dll` version is still missing, will automatically run [RDPWrapOffsetFinder](https://github.com/llccd/RDPWrapOffsetFinder) to generate offsets for your specific build.
> Update doesn't help, it still shows `[not supported]`.
@ -187,10 +228,41 @@ Check the [issues](https://github.com/sjackson0109/rdpwrap/issues) section to se
### Change log:
#### 2026.03.31
- **Repository housekeeping** — removed six obsolete files: `res/legacy.install.bat`, `res/clearres.bat`, `res/build_wxs.bat`, `res/RDPWInst.wxs` (WiX v3.11 MSI, unmaintained), `res/rdpwrap-ini-kb.txt` (stale 2018 INI snapshot), and empty `src-csharp/RDPWrap.Common/` stub directory
- `bin/install.bat`, `bin/uninstall.bat`, `bin/update.bat` — rewritten with architecture detection; now invoke the correct `RDPWInst_x64.exe` / `RDPWInst_x86.exe` (previously called non-existent `RDPWInst.exe`); later removed in favour of the MSI installer
- CI/CD: `build-cpp.yml`, `build-csharp.yml`, `build-offsetfinder.yml` — standalone `release` jobs removed; `build-and-release.yml` is now the sole release publisher, eliminating duplicate partial releases on tag pushes
- `build-csharp.yml` runner harmonised to `windows-2022`; hardcoded `signtool.exe` SDK path replaced with glob-based discovery
- `build-and-release.yml` — added **embedded-resource staging** step (copies built DLLs + INI into `RDPWInst/Resources/` before `dotnet publish`, enabling offline bundled install); added **SHA-256 audit log** for third-party sergiye binaries; `msi/rdpwrap-arm-kb.ini` added to release assets
- **ARM64 support**`Release|ARM64` added to `src-x86-x64-Fusix/RDPWrap.vcxproj`; `build-cpp.yml` and `build-and-release.yml` now build and ship `rdpwrap_arm64.dll`; `build-csharp.yml` and `build-and-release.yml` publish `RDPWInst_arm64.exe`, `RDPConf_arm64.exe`, `RDPCheck_arm64.exe`; `Directory.Build.props` adds `arm64` to `Platforms`
- **WiX v5 MSI packaging** — new `msi/RDPWInst.wxs` (WiX v5 schema v4, dual-arch, MajorUpgrade) and `msi/RDPWInst.wixproj`; replaces the deleted v3.11 artefacts; MSI build steps are now inlined into `build-and-release.yml` (a standalone `build-msi.yml` was created then removed as redundant)
- **Self-contained publish**`build-and-release.yml` produces `*_x64_sc.exe`, `*_x86_sc.exe`, `*_arm64_sc.exe` for all three C# tools and bundles them into `RDPWrapper-SelfContained.zip`; users without .NET 10 Desktop Runtime can use these
- **Version stamp automation**`build-and-release.yml` computes a `yyyy.M.d` stamp and passes `-p:Version=` to every `dotnet publish` call; `Directory.Build.props` documents the CI override pattern
- **Changelog automation**`build-and-release.yml` now includes a `Generate changelog` step that queries merged PRs since the previous release and embeds them in the GitHub Release body
- **Dependabot**`.github/dependabot.yml` added for `github-actions` and `nuget` ecosystems (weekly, Monday schedule)
- **Sergiye hash-pin scaffold**`tools/sergiye-hashes.json` created; `build-and-release.yml` validates downloaded `rdpWrapper_*.exe` hashes against this file when populated
- **Code-signing guide** added — `docs/CODE-SIGNING.md` documents certificate acquisition, PFX export, base64 encoding, and GitHub secret upload; the signing step in `build-and-release.yml` and `build-csharp.yml` fires automatically once `CODESIGN_CERT_BASE64` and `CODESIGN_CERT_PASSWORD` secrets are set
- **Sergiye hash pins live**`tools/update-sergiye-hashes.ps1` automation script created; `tools/sergiye-hashes.json` populated with verified SHA-256 hashes for `sergiye/rdpWrapper` release `2.10`; `build-and-release.yml` hash-verification step is now enforcing the pinned values
- **Screenshot infrastructure**`docs/images/` directory and capture guide created; README restored with four-cell screenshot table using relative in-repo paths (PNG files pending first capture)
- **Submodule shallow-clone**`shallow = true` added to `.gitmodules`; `docs/SUBMODULE-UPDATE.md` documents check-out, update, and rollback procedures (`RDPOffsetFinder` is already pinned to `v0.9`)
- **`tools/` reference added to `docs/`** — `update-sergiye-hashes.ps1` is self-documenting via `Get-Help`; `CODE-SIGNING.md`, `SUBMODULE-UPDATE.md`, `images/README.md` added to `docs/`
#### 2026.03.30
- **Full C# port complete**`RDPWInst`, `RDPConf`, `RDPCheck`, and shared library all ported from Delphi to C# / .NET 10; Delphi is no longer required to build
- Obsolete Delphi source folders removed (`src-installer/`, `src-rdpcheck/`, `src-rdpconfig/`, `src-x86-binarymaster/`)
- Shared library renamed from `RDPWrap.Common/` to `RDPWrap/` for a cleaner folder layout; namespace `RDPWrap.Common` preserved for source compatibility
- **[llccd/RDPWrapOffsetFinder](https://github.com/llccd/RDPWrapOffsetFinder) added as a git submodule** at `src-csharp/RDPOffsetFinder/` (including nested `zydis``zycore` submodules) — offset finder now built from source rather than fetching pre-built binaries
- Pre-built binary cache (`tools/RDPWrapOffsetFinder/`) and `update-finder-tools.yml` workflow removed; `build-and-release.yml` now builds the offset finder directly from the submodule
- New workflow [`build-offsetfinder.yml`](.github/workflows/build-offsetfinder.yml) — builds `RDPWrapOffsetFinder` + `Zydis.dll` for x64 and Win32 from source on version tag push
- New workflow [`build-csharp.yml`](.github/workflows/build-csharp.yml) — publishes self-contained single-file x64/x86 EXEs on version tag push; optional `signtool.exe` code-signing step wired to `CODESIGN_CERT_BASE64` / `CODESIGN_CERT_PASSWORD` repository secrets
- [`build-and-release.yml`](.github/workflows/build-and-release.yml) updated — checkout uses `submodules: recursive`; builds and bundles `RDPWInst`, `RDPConf`, `RDPCheck`, and `RDPWrapOffsetFinder` (all x64 + x86) alongside the existing DLL and INI assets
- Author metadata (`Simon Jackson / @sjackson0109`, copyright, repository URL) embedded into all four C# assemblies via `Directory.Build.props`
- `src-csharp/Directory.Build.props` targets `net10.0-windows`; `x86` and `x64` platforms; `Nullable` + `ImplicitUsings` enabled
#### 2026.03.29
- Fork maintained by [@sjackson0109](https://github.com/sjackson0109)
- INI source redirected from unmaintained stascorp upstream to this repository's GitHub Releases
- **CI/CD pipeline added** — [`publish-ini.yml`](.github/workflows/publish-ini.yml) publishes `rdpwrap.ini` and the `RDPWrapOffsetFinder` tools as release assets on every INI change
- **CI/CD pipeline added** — [`build-and-release.yml`](.github/workflows/build-and-release.yml) publishes `rdpwrap.ini` and the `RDPWrapOffsetFinder` tools as release assets on every INI change
- **CI/CD pipeline added** — [`build-cpp.yml`](.github/workflows/build-cpp.yml) builds `rdpwrap_x64.dll` / `rdpwrap_x86.dll` via MSVC v143 (VS 2022) on version tag push
- **Auto-offset generation** added to `RDPWInst.dpr` — on install (`-i`) and update (`-w`), if the running `termsrv.dll` version is absent from the INI the installer downloads `RDPWrapOffsetFinder` from release assets and appends the generated `[x.x.xxxxx.xxxxx]` section automatically; inspired by [sergiye/rdpWrapper](https://github.com/sergiye/rdpWrapper)
- New installer helpers: `DownloadFileToDisk`, `INIHasSection`, `TryAutoGenerateOffsets`
@ -476,15 +548,15 @@ Check the [issues](https://github.com/sjackson0109/rdpwrap/issues) section to se
- Windows Server 2016 Technical Preview
Installation instructions:
- Download latest release binaries and unpack files
- Right-click on **`install.bat`** and select Run as Administrator
- Download `RDPWrapper-<version>.msi` from the [GitHub Releases](https://github.com/sjackson0109/rdpwrap/releases) page
- Double-click the MSI and accept the UAC prompt — the installer detects your architecture automatically
- See command output for details
To update INI file:
- Right-click on **`update.bat`** and select Run as Administrator
- Open an Administrator command prompt in `%ProgramFiles%\RDP Wrapper`
- Run `RDPWInst_x64.exe -w` (or `RDPWInst_x86.exe -w` on 32-bit Windows)
- See command output for details
To uninstall:
- Go to the directory where you extracted the files
- Right-click on **`uninstall.bat`** and select Run as Administrator
- See command output for details
- Open **Add or Remove Programs** and uninstall **RDP Wrapper Library**
- Alternatively run `msiexec /x RDPWrapper-<version>.msi` from an elevated prompt

@ -0,0 +1,3 @@
# TODO
All items complete. Nothing pending.

@ -1,14 +0,0 @@
@echo off
if not exist "%~dp0RDPWInst.exe" goto :error
"%~dp0RDPWInst" -i -o
echo ______________________________________________________________
echo.
echo You can check RDP functionality with RDPCheck program.
echo Also you can configure advanced settings with RDPConf program.
echo.
goto :anykey
:error
echo [-] Installer executable not found.
echo Please extract all files from the downloaded package or check your anti-virus.
:anykey
pause

@ -1,10 +0,0 @@
@echo off
if not exist "%~dp0RDPWInst.exe" goto :error
"%~dp0RDPWInst" -u
echo.
goto :anykey
:error
echo [-] Installer executable not found.
echo Please extract all files from the downloaded package or check your anti-virus.
:anykey
pause

@ -1,10 +0,0 @@
@echo off
if not exist "%~dp0RDPWInst.exe" goto :error
"%~dp0RDPWInst" -w
echo.
goto :anykey
:error
echo [-] Installer executable not found.
echo Please extract all files from the downloaded package or check your anti-virus.
:anykey
pause

@ -0,0 +1,255 @@
# Building RDP Wrapper Locally
This document covers the full local build pipeline — from prerequisites to the final `./build/` artefacts.
---
## Prerequisites
| Component | Version | Notes |
|---|---|---|
| Windows 10/11 x64 | any | Host OS |
| .NET SDK | 10.0+ | `winget install Microsoft.DotNet.SDK.10` |
| Visual Studio 2019 Build Tools | 16.x | For C++ DLL only |
| MSVC v142 toolset | 14.29+ | Installed via VS Build Tools installer |
| Windows SDK | 10.0.19041+ | Installed via VS Build Tools installer |
> **ARM64 DLL note:** Building `rdpwrap_arm64.dll` locally requires Visual Studio 2022
> with the "MSVC v143 — VS 2022 C++ ARM64 build tools" component. The GitHub Actions
> CI uses a hosted `windows-latest` runner which provides this. For local work, the
> x64 and Win32 DLLs are sufficient for testing.
---
## 1. Clone the repository
```powershell
git clone --recurse-submodules https://github.com/<owner>/rdpwrap.git
cd rdpwrap
```
If you already cloned without `--recurse-submodules`:
```powershell
git submodule update --init --recursive
```
---
## 2. Generate application icons
The C# tool icons are generated programmatically via a helper script and are **not**
committed to source control. Run this once after cloning (and again if you delete them):
```powershell
.\tools\make-icons.ps1
```
This creates:
- `src-csharp/RDPConf/app.ico` — blue "C" icon for RDPConf
- `src-csharp/RDPCheck/app.ico` — green "K" icon for RDPCheck
Both files are `.gitignore`-exempt (not listed) so they persist in your working tree.
---
## 3. Build the C++ DLL (`rdpwrap.dll`)
### Locate MSBuild
Visual Studio 2019 Build Tools installs MSBuild to a non-standard path.
Add it to your session PATH once (or use the full path as shown below):
```powershell
$msbuild = "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\amd64\MSBuild.exe"
```
> If you have Visual Studio IDE installed instead of Build Tools, the path is:
> `C:\Program Files\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\amd64\MSBuild.exe`
### One-time vcxproj fix
The Windows SDK 10.0.19041 headers emit a C2338 packing warning that is treated as an
error under `/WX`. It has **no behavioural effect** — the fix is already applied in
`src-x86-x64-Fusix/RDPWrap.vcxproj` via the `WINDOWS_IGNORE_PACKING_MISMATCH` define.
No manual action is required.
### Build x64 and Win32
```powershell
$msbuild = "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\amd64\MSBuild.exe"
& $msbuild src-x86-x64-Fusix\RDPWrap.sln `
/p:Configuration=Release /p:Platform=x64 `
/p:PlatformToolset=v142 `
/p:WindowsTargetPlatformVersion=10.0.19041.0 `
/m /v:m
& $msbuild src-x86-x64-Fusix\RDPWrap.sln `
/p:Configuration=Release /p:Platform=Win32 `
/p:PlatformToolset=v142 `
/p:WindowsTargetPlatformVersion=10.0.19041.0 `
/m /v:m
```
> Expect `C4244` / `C4267` type-conversion warnings from the Zydis disassembler
> submodule — these are benign and can be ignored.
### Output locations
| Platform | Output path |
|---|---|
| x64 | `src-x86-x64-Fusix/x64/Release/RDPWrap.dll` |
| Win32 | `src-x86-x64-Fusix/Release/RDPWrap.dll` |
---
## 4. Build C# tools
All three C# tools are published as self-contained single-file executables using
`dotnet publish`.
### Architectures
| RID | Description |
|---|---|
| `win-x64` | 64-bit Intel/AMD |
| `win-x86` | 32-bit Intel/AMD |
| `win-arm64` | ARM64 (cross-compiled, no native toolchain needed) |
### Commands
```powershell
$TOOLS = @("RDPConf", "RDPCheck", "RDPWInst")
$RIDS = @("win-x64", "win-x86", "win-arm64")
foreach ($tool in $TOOLS) {
foreach ($rid in $RIDS) {
$arch = $rid -replace "win-", ""
$out = "build\staging\$tool\$rid"
dotnet publish "src-csharp\$tool\$tool.csproj" `
-c Release -r $rid `
--self-contained true `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:PublishTrimmed=false `
-p:Version=1.0.0 `
-o $out
}
}
```
### Output
After publishing, executables are in `build/staging/<Tool>/<RID>/`:
```
build\staging\RDPConf\win-x64\RDPConf.exe
build\staging\RDPConf\win-x86\RDPConf.exe
build\staging\RDPConf\win-arm64\RDPConf.exe
build\staging\RDPCheck\win-x64\RDPCheck.exe
...
```
---
## 5. Assemble `./build/`
Copy all artefacts to a flat `./build/` directory:
```powershell
New-Item -Force -ItemType Directory build | Out-Null
# DLLs
Copy-Item src-x86-x64-Fusix\x64\Release\RDPWrap.dll build\rdpwrap_x64.dll
Copy-Item src-x86-x64-Fusix\Release\RDPWrap.dll build\rdpwrap_x86.dll
# C# executables
foreach ($tool in @("RDPConf","RDPCheck","RDPWInst")) {
foreach ($rid in @("win-x64","win-x86","win-arm64")) {
$arch = $rid -replace "win-",""
Copy-Item "build\staging\$tool\$rid\$tool.exe" `
"build\${tool}_${arch}.exe"
}
}
# Cleanup staging
Remove-Item -Recurse -Force build\staging
```
### Final `./build/` layout
```
build/
rdpwrap_x64.dll # x64 termsrv.dll hook
rdpwrap_x86.dll # x86 termsrv.dll hook
RDPConf_x64.exe # GUI config tool (x64)
RDPConf_x86.exe # GUI config tool (x86)
RDPConf_arm64.exe # GUI config tool (ARM64)
RDPCheck_x64.exe # RDP loopback tester (x64)
RDPCheck_x86.exe # RDP loopback tester (x86)
RDPCheck_arm64.exe # RDP loopback tester (ARM64)
RDPWInst_x64.exe # CLI installer (x64)
RDPWInst_x86.exe # CLI installer (x86)
RDPWInst_arm64.exe # CLI installer (ARM64)
```
> `./build/` is listed in `.gitignore` — artefacts are not committed.
---
## 6. Automated script
All steps 25 are automated in `tools/build-local.ps1`.
Run it from the repo root:
```powershell
.\tools\build-local.ps1
```
Optional flag to skip the DLL rebuild (faster during C# iteration):
```powershell
.\tools\build-local.ps1 -SkipCpp
```
---
## 7. Troubleshooting
### MSBuild not found
Verify VS 2019 Build Tools are installed.
Open the **Visual Studio Installer** → Modify → ensure
"Desktop development with C++" and "MSVC v142 build tools" are checked.
### ARM64 DLL: `error MSB8013`
No ARM64 cross-compiler found. Either install VS 2022 with ARM64 tools or skip ARM64
(`-SkipArm64Dll` flag in `build-local.ps1`). The ARM64 DLL is produced by CI.
### `error C2338: Windows headers require the default packing option`
`WINDOWS_IGNORE_PACKING_MISMATCH` is missing from `PreprocessorDefinitions`.
Run the following once then rebuild:
```powershell
(Get-Content src-x86-x64-Fusix\RDPWrap.vcxproj) `
-replace '(<PreprocessorDefinitions>)([^<]+)(<)', `
'$1$2;WINDOWS_IGNORE_PACKING_MISMATCH$3' |
Set-Content src-x86-x64-Fusix\RDPWrap.vcxproj
```
### `dotnet publish` fails with SDK not found
Ensure .NET 10 SDK is installed: `dotnet --version` should print `10.x.x`.
---
## Appendix: CI vs local comparison
| Feature | Local build | GitHub Actions CI |
|---|---|---|
| x64 DLL | ✅ | ✅ |
| x86 DLL | ✅ | ✅ |
| ARM64 DLL | ❌ (needs VS 2022) | ✅ |
| C# x64/x86/arm64 | ✅ (cross-compiled) | ✅ |
| Code signing | ❌ | ✅ (if cert configured) |
| GitHub Release | ❌ | ✅ (on tag push) |

@ -0,0 +1,100 @@
# Code Signing Guide
Both `build-and-release.yml` and `build-csharp.yml` include a code-signing step that is already wired up and waiting. The step fires automatically as soon as the two secrets below are present in the repository — no workflow edits are needed.
---
## What gets signed
All six framework-dependent executables and all six self-contained executables produced per release:
| File | Contents |
|---|---|
| `RDPWInst_x64.exe`, `RDPWInst_x86.exe`, `RDPWInst_arm64.exe` | CLI installer |
| `RDPConf_x64.exe`, `RDPConf_x86.exe`, `RDPConf_arm64.exe` | GUI configuration tool |
| `RDPCheck_x64.exe`, `RDPCheck_x86.exe`, `RDPCheck_arm64.exe` | GUI connection tester |
| `*_sc.exe` variants | Self-contained copies of the above |
The signing step runs on every `build-and-release.yml` and `build-csharp.yml` triggered build (tag push and manual dispatch), and is **skipped silently** when the secrets are absent.
---
## Obtaining a code-signing certificate
### Option A — Commercial certificate (recommended for public distribution)
Purchase an **EV (Extended Validation) Code Signing Certificate** from a trusted CA:
- [DigiCert Code Signing](https://www.digicert.com/signing/code-signing-certificates)
- [Sectigo (Comodo)](https://sectigo.com/ssl-certificates-tls/code-signing)
- [GlobalSign](https://www.globalsign.com/en/code-signing-certificate/)
> **Windows SmartScreen** initially blocks unsigned or low-reputation executables. A commercial EV certificate builds reputation immediately; a standard OV certificate requires time to accumulate reputation through user downloads. Self-signed certificates (Option B) will always trigger SmartScreen warnings.
### Option B — Self-signed certificate (testing / internal use only)
```powershell
# Run in an elevated PowerShell session
$cert = New-SelfSignedCertificate `
-Type CodeSigningCert `
-Subject "CN=RDP Wrapper" `
-KeySpec Signature `
-KeyAlgorithm RSA `
-KeyLength 4096 `
-HashAlgorithm SHA256 `
-CertStoreLocation "Cert:\CurrentUser\My" `
-NotAfter (Get-Date).AddYears(3)
# Export to PFX (set a strong password)
$pw = Read-Host "PFX password" -AsSecureString
Export-PfxCertificate -Cert $cert -FilePath "rdpwrap-codesign.pfx" -Password $pw
```
---
## Preparing the PFX for GitHub Actions
```powershell
# Base64-encode the PFX so it can be stored as a secret
$b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes(".\rdpwrap-codesign.pfx"))
$b64 | Set-Clipboard
Write-Host "Base64 PFX copied to clipboard"
```
---
## Adding the secrets to GitHub
Navigate to: **Settings → Secrets and variables → Actions → New repository secret**
| Secret name | Value |
|---|---|
| `CODESIGN_CERT_BASE64` | Paste the base64-encoded PFX string |
| `CODESIGN_CERT_PASSWORD` | The password chosen when exporting the PFX |
> ⚠️ **Security:** Never commit the `.pfx` file or raw base64 string to the repository. Revoke and reissue the certificate if it is accidentally exposed.
---
## Verifying a signed executable
After a signed release is published:
```powershell
# Check signature status
Get-AuthenticodeSignature .\RDPWInst_x64.exe | Format-List
# Expected output (commercial cert):
# Status : Valid
# SignerCertificate : [CN=Your Name, O=Your Org, ...]
# TimeStamperCertificate : [CN=DigiCert Timestamp 2023, ...]
```
---
## Renewing / rotating the certificate
1. Export the new PFX and base64-encode it (see above).
2. Update both `CODESIGN_CERT_BASE64` and `CODESIGN_CERT_PASSWORD` via **Settings → Secrets**.
3. No workflow changes are needed — the `Sign C# executables` step always reads from secrets at runtime.
4. Remove the old PFX from your local machine and revoke the old certificate with the CA if it has not expired.

@ -4,7 +4,88 @@ This guide explains the technical process for reverse engineering new Windows bu
## Overview
When Microsoft releases new Windows updates, the `termsrv.dll` file changes, and RDP Wrapper needs updated offset configurations to function properly. This document outlines the manual reverse engineering process required to find these offsets.
When Microsoft releases new Windows updates, the `termsrv.dll` file changes, and RDP Wrapper needs updated offset configurations to function properly. This document covers **two paths**:
1. **[Quick path — RDPWrapOffsetFinder (recommended)](#quick-path-rdpwrapoffsetfinder-automated):** the compiled tool bundled in every release can generate a valid INI section in seconds on most builds.
2. **[Manual path — disassembly and reverse engineering](#manual-path-disassembly-and-reverse-engineering):** used when the automated tool cannot find all offsets for a new or unusual build.
---
## Quick path: RDPWrapOffsetFinder (automated)
> **TL;DR** — run the tool, copy the generated INI block, open a PR.
RDPWrapOffsetFinder (by **llccd**, bundled in every release as `RDPWrapOffsetFinder_x64.exe` and `RDPWrapOffsetFinder_x86.exe`) performs automated static analysis on the installed `termsrv.dll` and emits a ready-to-use INI section.
### Step A: Run RDPWrapOffsetFinder
```powershell
# Locate termsrv.dll version (so you know what tag to add)
(Get-Item C:\Windows\System32\termsrv.dll).VersionInfo.ProductVersion
# Run the tool (run as Administrator; it reads the locked DLL via sharing flags)
.\RDPWrapOffsetFinder_x64.exe
```
On success the tool prints an INI block similar to:
```ini
[10.0.26100.3915]
SingleUserPatch.x64=1
SingleUserOffset.x64=0A1B2C
...
```
If the tool exits with `Unsupported version`, try the nosym (no-symbols) variant:
```powershell
.\RDPWrapOffsetFinder_nosym_x64.exe
```
### Step B: Use the autoupdate helper (optional)
The `autoupdate/` directory in `src-csharp/RDPOffsetFinder/` contains a convenience script that wraps the tool, appends the output to a local copy of `rdpwrap.ini`, and validates the result:
```batch
cd src-csharp\RDPOffsetFinder\RDPWrapOffsetFinder\autoupdate
autoupdate.bat
```
The script will:
1. Run `RDPWrapOffsetFinder.exe` against the current system's `termsrv.dll`.
2. Merge the new section into `rdpwrap_template.ini`.
3. Print the diff so you can review before committing.
### Step C: Validate the generated block
Before opening a pull request:
```powershell
# Quick sanity check — restart Terminal Services with the new INI
net stop TermService
Copy-Item .\rdpwrap_updated.ini "C:\Program Files\RDP Wrapper\rdpwrap.ini"
net start TermService
# Visual validation
.\RDPCheck.exe # should show "Installed", "Listening", and "Supported"
```
### Step D: Submit a pull request
1. Add (or update) the version block in `msi/rdpwrap.ini`.
2. Commit with message: `ini: add support for 10.0.XXXXX.YYYY`.
3. Open a pull request against the `main` branch using the [bug_report](.github/ISSUE_TEMPLATE/bug_report.yml) template.
Include: the output of `RDPWrapOffsetFinder.exe`, your `termsrv.dll` version, and a screenshot of `RDPCheck.exe` showing "Supported".
> **Security note:** Never attach the actual `termsrv.dll` binary — this would infringe Microsoft's licence. The version string and SHA-256 hash are sufficient for reproducibility.
---
## Manual path: disassembly and reverse engineering
Use this path when:
- RDPWrapOffsetFinder cannot find all offsets (prints partial output or crashes), **or**
- You want to understand *why* a patch works, not just *where* to apply it.
## Prerequisites

@ -0,0 +1,101 @@
# Submodule Update Guide
This repository includes one git submodule (with nested sub-submodules):
| Submodule | Upstream | Current tag |
|---|---|---|
| `src-csharp/RDPOffsetFinder` | [llccd/RDPWrapOffsetFinder](https://github.com/llccd/RDPWrapOffsetFinder) | `v0.9` |
| `src-csharp/RDPOffsetFinder/zydis` | [zyantific/zydis](https://github.com/zyantific/zydis) | (pinned by upstream) |
| `src-csharp/RDPOffsetFinder/zydis/dependencies/zycore` | [zyantific/zycore-c](https://github.com/zyantific/zycore-c) | (pinned by upstream) |
The nested `zydis` and `zycore` submodule commits are controlled by llccd's repository — update only the outer submodule and the inner ones follow automatically.
---
## Cloning with submodules
```bash
# Full clone (recommended for builds)
git clone --recurse-submodules https://github.com/sjackson0109/rdpwrap
# If you already cloned without --recurse-submodules:
git submodule update --init --recursive
```
---
## Checking current submodule version
```powershell
git submodule status --recursive
# Expected output (one line per submodule):
# 68da37acab6593c329776644944f55695a131731 src-csharp/RDPOffsetFinder (v0.9)
# 5a68f639e4f01604cc7bfc8d313f583a8137e3d3 src-csharp/RDPOffsetFinder/zydis (...)
# fb69402566a15a719e5df7a64a3db95105590b7e src-csharp/RDPOffsetFinder/zydis/dependencies/zycore (...)
```
---
## Updating to a new upstream release
> **Do this only when a new tagged release of [llccd/RDPWrapOffsetFinder](https://github.com/llccd/RDPWrapOffsetFinder) is published** and you have verified that the new version produces correct INI sections for a known Windows build.
```powershell
# 1. Fetch all tags from upstream
cd src-csharp/RDPOffsetFinder
git fetch --tags origin
# 2. List available tags to find the new release
git tag --list --sort=-version:refname | Select-Object -First 10
# 3. Check out the desired tag
git checkout <new-tag> # e.g. v1.0
# 4. Update nested submodules to match the new tag's references
git submodule update --init --recursive
# 5. Return to repo root and record the new pointer
cd ../..
git add src-csharp/RDPOffsetFinder
git commit -m "chore: update RDPOffsetFinder submodule to <new-tag>"
git push
```
---
## Verifying the updated submodule builds
```powershell
# From repo root — builds x64 and x86 offset finder binaries
cd src-csharp/RDPOffsetFinder
cmake -B build-x64 -A x64 .
cmake --build build-x64 --config Release
cmake -B build-x86 -A Win32 .
cmake --build build-x86 --config Release
```
Or trigger the `build-offsetfinder.yml` workflow on your push branch to let CI validate it.
---
## Rollback
If the update causes build failures, revert the submodule pointer:
```powershell
git revert HEAD # creates a revert commit
git push
```
Or manually:
```powershell
cd src-csharp/RDPOffsetFinder
git checkout v0.9 # previous known-good tag
cd ../..
git add src-csharp/RDPOffsetFinder
git commit -m "chore: rollback RDPOffsetFinder submodule to v0.9"
git push
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -0,0 +1,51 @@
# Screenshots — `docs/images/`
This directory holds in-repo screenshot assets referenced by the project README.
---
## Files present
The following PNG screenshots are committed and referenced by the README:
| Filename | Contents |
|---|---|
| `RDPWrapperConfig.png` | RDPConf.exe configuration window |
| `RDPWrapperCheck.png` | RDPCheck.exe showing green "Supported" status |
| `RDPWrapperCheckWarning.png` | RDPCheck.exe showing "Warning" / not-yet-supported state |
| `RDPWrapperMSI1.png` | MSI installer welcome / licence screen |
| `RDPWrapperMSI2.png` | MSI installer completion screen |
---
## Files that would still be welcome
The following screenshots would improve documentation coverage but are not blocking any CI step:
| Filename | What to capture |
|---|---|
| `RDPWrapperConfig-advanced.png` | RDPConf.exe Advanced / License tab — SL policy and licensing status fields |
| `rdpwrapper-gui.png` | sergiye/rdpWrapper GUI — main window with connection status |
| `install-success.png` | Terminal output of `RDPWInst_x64.exe -i -o` completing successfully |
---
## Capture tips
- Use **1:1 scaling** (100 % DPI) so button labels are legible at small display size.
- Crop to exclude desktop wallpaper — show only the tool window, with a 4 px neutral border all round.
- Use PNG format (lossless); aim for < 200 KB each (resize if needed 900 px wide is more than enough).
- Redact any username, hostname, or licence strings visible in the window before committing.
- Filename convention: `tool-state.png` — all lowercase, hyphen-delimited.
---
## Adding a new screenshot
```bash
# After copying the PNG here:
git add docs/images/<filename>.png
git commit -m "docs: add <description> screenshot"
```
The README references each file with a relative path, so the image will render automatically on GitHub once committed.

@ -0,0 +1,24 @@
<Project Sdk="WixToolset.Sdk/5.0.2">
<!--
RDP Wrapper MSI project — WiX v5 (dotnet build)
Build once per architecture:
dotnet build msi/RDPWInst.wixproj -c Release /p:Platform=x64 /p:PackageVersion=yy.M.d
dotnet build msi/RDPWInst.wixproj -c Release /p:Platform=x86 /p:PackageVersion=yy.M.d
dotnet build msi/RDPWInst.wixproj -c Release /p:Platform=arm64 /p:PackageVersion=yy.M.d
Each build expects arch-specific inputs alongside this .wixproj:
RDPWInst_<arch>.exe RDPConf_<arch>.exe RDPCheck_<arch>.exe
rdpwrap_<arch>.dll rdpwrap.ini
-->
<PropertyGroup>
<!-- OutputName includes arch so all three MSIs can coexist in the same output dir -->
<OutputName>RDPWrapper-$(Platform)</OutputName>
<!-- Version is overridden on the CLI: /p:PackageVersion=yy.M.d (e.g. 26.4.1) -->
<PackageVersion>26.3.31</PackageVersion>
<!-- Expose PackageVersion to the WiX preprocessor as $(var.PackageVersion) -->
<DefineConstants>PackageVersion=$(PackageVersion)</DefineConstants>
<!-- Suppress ICE warnings that do not apply to this package type -->
<SuppressIces>ICE61</SuppressIces>
</PropertyGroup>
</Project>

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
RDP Wrapper — MSI installer definition (WiX v5 / schema v4)
Parameterised by MSBuild Platform: build once per arch.
Build via:
dotnet build msi/RDPWInst.wixproj -c Release /p:Platform=x64 /p:PackageVersion=yy.M.d
dotnet build msi/RDPWInst.wixproj -c Release /p:Platform=x86 /p:PackageVersion=yy.M.d
dotnet build msi/RDPWInst.wixproj -c Release /p:Platform=arm64 /p:PackageVersion=yy.M.d
Required inputs alongside this file (produced by CI / build-local.ps1):
RDPWInst_<arch>.exe RDPConf_<arch>.exe RDPCheck_<arch>.exe
rdpwrap_<arch>.dll
rdpwrap.ini (shared — same file for all arches)
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<!--
$(var.Platform) is automatically set by WiX from the MSBuild Platform property.
x86 installs to Program Files (x86); x64/arm64 install to Program Files.
-->
<?if $(var.Platform) = "x64" ?>
<?define Arch = "x64" ?>
<?define UpgradeCode = "6623f60c-e84f-41e7-a55b-f42116500064" ?>
<?define ProgramFilesDir = "ProgramFiles6432Folder" ?>
<?elseif $(var.Platform) = "x86" ?>
<?define Arch = "x86" ?>
<?define UpgradeCode = "6623f60c-e84f-41e7-a55b-f42116500032" ?>
<?define ProgramFilesDir = "ProgramFilesFolder" ?>
<?else ?>
<?define Arch = "arm64" ?>
<?define UpgradeCode = "6623f60c-e84f-41e7-a55b-f4211650aa64" ?>
<?define ProgramFilesDir = "ProgramFiles6432Folder" ?>
<?endif ?>
<Package
Name="RDP Wrapper Library ($(var.Arch))"
Version="$(var.PackageVersion)"
Manufacturer="Simon Jackson (@sjackson0109)"
UpgradeCode="$(var.UpgradeCode)"
Scope="perMachine"
InstallerVersion="500"
Compressed="yes">
<!-- Allow upgrades, downgrades, and same-version reinstalls without blocking.
Schedule=afterInstallInitialize ensures the old product is fully removed
before new files are written, preventing component-ownership conflicts. -->
<MajorUpgrade
Schedule="afterInstallInitialize"
AllowDowngrades="yes"
IgnoreRemoveFailure="yes" />
<MediaTemplate EmbedCab="yes" />
<!-- ── Installation directory ─────────────────────────────────────────── -->
<StandardDirectory Id="$(var.ProgramFilesDir)">
<Directory Id="INSTALLFOLDER" Name="RDP Wrapper" />
</StandardDirectory>
<!-- ── Feature ────────────────────────────────────────────────────────── -->
<Feature Id="ProductFeature" Title="RDP Wrapper" Level="1">
<ComponentGroupRef Id="ProductComponents" />
</Feature>
<!-- ── Custom actions: run arch-native RDPWInst ──────────────────────── -->
<!--
-f (force): silently uninstalls any existing installation (bat- or
MSI-managed) before re-installing. Prevents the "already installed"
error when upgrading from a previous MSI or a manual install.
-->
<CustomAction
Id="InstallWrap"
FileRef="RDPWInstExe"
ExeCommand="-i -o -f"
Execute="deferred"
Impersonate="no"
Return="check" />
<CustomAction
Id="UninstallWrap"
FileRef="RDPWInstExe"
ExeCommand="-u"
Execute="deferred"
Impersonate="no"
Return="check" />
<InstallExecuteSequence>
<!-- Run install wrapper after files land; skip only when THIS product is
being removed as the OLD side of an upgrade (UPGRADINGPRODUCTCODE set). -->
<Custom Action="InstallWrap" After="InstallFiles" Condition="NOT UPGRADINGPRODUCTCODE" />
<Custom Action="UninstallWrap" Before="RemoveFiles" Condition="REMOVE AND NOT UPGRADINGPRODUCTCODE" />
</InstallExecuteSequence>
</Package>
<!-- ── Component group ─────────────────────────────────────────────────── -->
<Fragment>
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
<!-- RDPWInst (installer / uninstaller tool) -->
<Component Id="RDPWInstExe_Comp" Guid="*">
<File Id="RDPWInstExe" Source="RDPWInst_$(var.Arch).exe" KeyPath="yes" />
</Component>
<!-- RDPConf (configuration GUI) -->
<Component Id="RDPConfExe_Comp" Guid="*">
<File Id="RDPConfExe" Source="RDPConf_$(var.Arch).exe" KeyPath="yes" />
</Component>
<!-- RDPCheck (RDP loopback tester) -->
<Component Id="RDPCheckExe_Comp" Guid="*">
<File Id="RDPCheckExe" Source="RDPCheck_$(var.Arch).exe" KeyPath="yes" />
</Component>
<!-- rdpwrap.dll (core DLL) -->
<Component Id="RdpwrapDll_Comp" Guid="*">
<File Source="rdpwrap_$(var.Arch).dll" KeyPath="yes" />
</Component>
<!-- rdpwrap.ini (offset database — same file for all arches) -->
<Component Id="RdpwrapIni_Comp" Guid="*">
<File Source="rdpwrap.ini" KeyPath="yes" />
</Component>
</ComponentGroup>
</Fragment>
</Wix>

@ -0,0 +1,5 @@
{
"msbuild-sdks": {
"WixToolset.Sdk": "5.0.2"
}
}

@ -2,7 +2,7 @@
; Do not modify without special knowledge
[Main]
Updated=2026-01-25
Updated=2026-04-02
LogFile=\rdpwrap.txt
SLPolicyHookNT60=1
SLPolicyHookNT61=1

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<?define ProductName="RDP Wrapper Library" ?>
<?define ProductVersion="1.6.2" ?>
<?define ProductCode="37ea5771-3352-4a52-9fac-9297331daebd"?>
<?define UpgradeCode="6623f60c-e84f-41e7-a55b-f421165deeb5"?>
<?define Manufacturer="Stas'M Corp. and contributors"?>
<Product Id="$(var.ProductCode)" Name="$(var.ProductName)" Language="1033" Version="$(var.ProductVersion)" Manufacturer="$(var.Manufacturer)" UpgradeCode="$(var.UpgradeCode)">
<Package InstallerVersion="200" Compressed="yes"/>
<Property Id="MSIFASTINSTALL" Value="1" />
<Property Id="DISABLEROLLBACK" Value="1" />
<MajorUpgrade AllowDowngrades="yes"/>
<Media Id="1" Cabinet="media1.cab" EmbedCab="yes"/>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="CommonAppDataFolder">
<Directory Id="PACKAGECACHE" Name="Package Cache">
<Directory Id="INSTALLLOCATION" Name="{$(var.ProductCode)}">
<Component Id="RDPWrap" Guid="affd77d1-b35c-46f3-a97f-1686dc57b8b8">
<File Id='RDPWInst' DiskId='1' Source='RDPWInst.exe'/>
</Component>
</Directory>
</Directory>
</Directory>
</Directory>
<Feature Id="RDPWrapInstall" Title="RDPWrapSetup" Level="1">
<ComponentRef Id="RDPWrap" />
</Feature>
<CustomAction Id='InstallAction' FileKey='RDPWInst' ExeCommand='-i -o' Execute='immediate' Return='check'/>
<CustomAction Id='UninstallAction' FileKey='RDPWInst' ExeCommand='-u' Execute='immediate' Return='check'/>
<CustomAction Id='UpdateAction' FileKey='RDPWInst' ExeCommand='-w' Execute='immediate' Return='check'/>
<!-- <CustomAction Id='ChangeAction' Directory='ProgramFilesFolder' ExeCommand='RDP Wrapper\RDPConf' Execute='immediate' Return='check'/>
<CustomAction Id='RepairAction' Directory='ProgramFilesFolder' ExeCommand='RDP Wrapper\RDPCheck' Execute='immediate' Return='check'/> -->
<InstallExecuteSequence>
<Custom Action='InstallAction' After='InstallFinalize'>NOT Installed AND NOT WIX_UPGRADE_DETECTED</Custom>
<Custom Action='UninstallAction' Before="RemoveFiles">REMOVE AND NOT UPGRADINGPRODUCTCODE</Custom>
<Custom Action='UpdateAction' Before="RemoveFiles">UPGRADINGPRODUCTCODE</Custom>
<!-- <Custom Action='ChangeAction' After='InstallFinalize'>Installed AND NOT REINSTALL AND NOT UPGRADINGPRODUCTCODE AND NOT REMOVE</Custom>
<Custom Action='RepairAction' After='InstallFinalize'>REINSTALL</Custom> -->
</InstallExecuteSequence>
</Product>
</Wix>

@ -1,3 +0,0 @@
@echo off
"%ProgramFiles%\WiX Toolset v3.11\bin\candle" RDPWInst.wxs
"%ProgramFiles%\WiX Toolset v3.11\bin\light" RDPWInst.wixobj

@ -1,19 +0,0 @@
@echo off
echo [FILENAMES]> clearres.txt
echo Exe=%1>> clearres.txt
echo SaveAs=%1>> clearres.txt
echo Log=>> clearres.txt
echo.>> clearres.txt
echo [COMMANDS]>> clearres.txt
echo -delete RCDATA,CHARTABLE,>> clearres.txt
echo -delete RCDATA,DVCLAL,>> clearres.txt
echo -delete RCDATA,PACKAGEINFO,>> clearres.txt
echo -delete CURSORGROUP,32761,>> clearres.txt
echo -delete CURSORGROUP,32762,>> clearres.txt
echo -delete CURSORGROUP,32763,>> clearres.txt
echo -delete CURSORGROUP,32764,>> clearres.txt
echo -delete CURSORGROUP,32765,>> clearres.txt
echo -delete CURSORGROUP,32766,>> clearres.txt
echo -delete CURSORGROUP,32767,>> clearres.txt
"C:\Program Files\Resource Hacker\ResHacker.exe" -script clearres.txt
del clearres.txt

@ -1,92 +0,0 @@
@echo off
setlocal EnableDelayedExpansion
echo RDP Wrapper Library Installer v1.0
echo Copyright (C) Stas'M Corp. 2013
echo.
set PROCESSOR_ARCHITECTURE | find "x86" > nul
if !errorlevel!==0 (
goto WOW64CHK
) else (
goto UNSUPPORTED
)
:WOW64CHK
echo [*] Check if running WOW64 subsystem...
set PROCESSOR_ARCHITEW6432 > nul
if !errorlevel!==0 (
goto UNSUPPORTED
) else (
goto SUPPORTED
)
:SUPPORTED
echo [+] Processor architecture is Intel x86 [supported]
goto INSTALL
:UNSUPPORTED
echo [-] Unsupported processor architecture
goto END
:INSTALL
echo [*] Installing...
if not exist rdpwrap.dll (
echo [-] Error: rdpwrap.dll file not found
goto END
)
echo [*] Copying file to Program Files...
md "%ProgramFiles%\RDP Wrapper"
xcopy /y rdpwrap.dll "%ProgramFiles%\RDP Wrapper\"
if not !errorlevel!==0 (
echo [-] Failed to copy rdpwrap.dll to Program Files folder
goto END
)
echo [*] Modifying registry...
reg add "HKLM\SYSTEM\CurrentControlSet\Services\TermService\Parameters" /v ServiceDll /t REG_EXPAND_SZ /d "%ProgramFiles%\RDP Wrapper\rdpwrap.dll" /f
if not !errorlevel!==0 (
echo [-] Failed to modify registry
goto END
)
echo [*] Setting firewall configuration...
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0 /f
netsh advfirewall firewall add rule name="Remote Desktop" dir=in protocol=tcp localport=3389 profile=any action=allow
netsh advfirewall firewall add rule name="Remote Desktop" dir=in protocol=udp localport=3389 profile=any action=allow
echo [*] Looking for TermService PID...
tasklist /SVC /FI "SERVICES eq TermService" | find "PID" /V
echo.
if !errorlevel!==0 (
goto DONE
) else (
goto SVCSTART
)
:SVCSTART
echo [*] TermService is stopped. Starting it...
sc config TermService start= auto | find "1060" > nul
if !errorlevel!==0 (
echo [-] TermService is not installed. You need to install it manually.
goto END
) else (
net start TermService
goto DONE
)
:DONE
echo [+] Installation complete!
echo Now reboot or restart service.
echo.
echo To reboot computer type:
echo shutdown /r
echo.
echo To restart TermService type:
echo taskkill /f /pid 1234 ^(replace 1234 with real PID which is shown above^)
echo net start TermService
echo.
echo If second method is used, and there are another services sharing svchost.exe,
echo you must start it too:
echo net start Service1
echo net start Service2
echo etc.
goto END
:END

File diff suppressed because it is too large Load Diff

@ -0,0 +1,44 @@
<Project>
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<Platforms>x86;x64;arm64</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>
<!--
Version is intentionally kept as a build-time default for local development.
CI overrides these three properties on the dotnet publish command line:
/p:Version=yyyy.M.d /p:AssemblyVersion=yyyy.M.d.0 /p:FileVersion=yyyy.M.d.0
so released binaries always embed the correct date-based version.
-->
<Version>$([System.DateTime]::Now.ToString("yyyy.M.d"))</Version>
<AssemblyVersion>$([System.DateTime]::Now.ToString("yyyy.M.d")).0</AssemblyVersion>
<FileVersion>$([System.DateTime]::Now.ToString("yyyy.M.d")).0</FileVersion>
<!-- Author / ownership metadata (embedded in every assembly) -->
<Authors>Simon Jackson</Authors>
<Company>Simon Jackson (@sjackson0109)</Company>
<Copyright>Copyright © 2026 Simon Jackson (@sjackson0109)</Copyright>
<Product>RDP Wrapper</Product>
<RepositoryUrl>https://github.com/sjackson0109/rdpwrap</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageProjectUrl>https://github.com/sjackson0109/rdpwrap</PackageProjectUrl>
<!--
Reproducible NuGet restores.
RestorePackagesWithLockFile=true causes dotnet to generate packages.lock.json
on the first restore if it doesn't exist, and to verify it on subsequent restores.
Commit the generated packages.lock.json files in each project directory so that
CI restores are deterministic. After committing lock files you can add
`- -locked-mode` to dotnet restore/publish calls in CI to enforce exact-match
and prevent silent dependency drift.
-->
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
</Project>

@ -0,0 +1,160 @@
// Copyright 2026 sjackson0109 — Apache License 2.0
//
// Hosts the MsRdpClient2 ActiveX control, connects to 127.0.0.2, and
// reports the disconnect reason, mirroring the Delphi RDPDisconnected handler.
using RDPWrap.Common;
namespace RDPCheck;
internal sealed class MainForm : Form
{
private readonly AxRdpClient2 _rdp;
// Registry values saved on load — restored on disconnect
private int _savedSecurityLayer;
private int _savedUserAuthentication;
// ── Constructor / Layout ──────────────────────────────────────────────────
public MainForm()
{
SuspendLayout();
Text = "RDP Wrapper Check";
ClientSize = new Size(800, 600);
StartPosition = FormStartPosition.CenterScreen;
FormBorderStyle = FormBorderStyle.Sizable;
_rdp = new AxRdpClient2
{
Dock = DockStyle.Fill
};
Controls.Add(_rdp);
ResumeLayout(false);
_rdp.Disconnected += OnRdpDisconnected;
Load += OnFormLoad;
}
// ── FormLoad — mirrors TFrm.FormCreate ────────────────────────────────────
private void OnFormLoad(object? sender, EventArgs e)
{
const string rdpTcpKey =
@"SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp";
_rdp.DisconnectedText = "Disconnected.";
_rdp.ConnectingText = "Connecting...";
_rdp.ConnectedStatusText = "Connected.";
_rdp.UserName = string.Empty;
_rdp.Server = "127.0.0.2";
// Read, then zero-out SecurityLayer / UserAuthentication
_savedSecurityLayer = RegistryHelper.ReadInt(rdpTcpKey, "SecurityLayer", 0);
_savedUserAuthentication = RegistryHelper.ReadInt(rdpTcpKey, "UserAuthentication", 0);
try
{
RegistryHelper.WriteInt(rdpTcpKey, "SecurityLayer", 0);
RegistryHelper.WriteInt(rdpTcpKey, "UserAuthentication", 0);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[RDP] Registry write failed: {ex.Message}");
}
// Read port
int port = RegistryHelper.ReadInt(rdpTcpKey, "PortNumber", 3389);
_rdp.SetPort(port);
// Subscribe to COM events now that the handle exists
_rdp.SubscribeEvents();
// Brief delay then connect — matches Delphi Sleep(1000); RDP.Connect;
Task.Delay(1000).ContinueWith(_ => Invoke((Action)_rdp.Connect));
}
// ── OnDisconnected — mirrors TFrm.RDPDisconnected ─────────────────────────
private void OnRdpDisconnected(object? sender, int discReason)
{
string errStr = discReason switch
{
0x001 => "Local disconnection.",
0x002 => "Disconnected by user.",
0x003 => "Disconnected by server.",
0x904 => "Socket closed.",
0xC08 => "Decompress error.",
0x108 => "Connection timed out.",
0xC06 => "Decryption error.",
0x104 => "DNS name lookup failure.",
0x508 => "DNS lookup failed.",
0xB06 => "Encryption error.",
0x604 => "Windows Sockets gethostbyname() call failed.",
0x208 => "Host not found error.",
0x408 => "Internal error.",
0x906 => "Internal security error.",
0xA06 => "Internal security error.",
0x506 => "The encryption method specified is not valid.",
0x804 => "Bad IP address specified.",
0x606 => "Server security data is not valid.",
0x406 => "Security data is not valid.",
0x308 => "The IP address specified is not valid.",
0x808 => "License negotiation failed.",
0x908 => "Licensing time-out.",
0x106 => "Out of memory.",
0x206 => "Out of memory.",
0x306 => "Out of memory.",
0x706 => "Failed to unpack server certificate.",
0x204 => "Socket connection failed.",
0x404 => "Windows Sockets recv() call failed.",
0x704 => "Time-out occurred.",
0x608 => "Internal timer error.",
0x304 => "Windows Sockets send() call failed.",
0xB07 => "The account is disabled.",
0xE07 => "The account is expired.",
0xD07 => "The account is locked out.",
0xC07 => "The account is restricted.",
0x1B07 => "The received certificate is expired.",
0x1607 => "The policy does not support delegation of credentials to the target server.",
0x2107 => "The server authentication policy does not allow connection requests using saved credentials. The user must enter new credentials.",
0x807 => "Login failed.",
0x1807 => "No authority could be contacted for authentication. The domain name of the authenticating party could be wrong, the domain could be unreachable, or there might have been a trust relationship failure.",
0xA07 => "The specified user has no account.",
0xF07 => "The password is expired.",
0x1207 => "The user password must be changed before logging on for the first time.",
0x1707 => "Delegation of credentials to the target server is not allowed unless mutual authentication has been achieved.",
0x2207 => "The smart card is blocked.",
0x1C07 => "An incorrect PIN was presented to the smart card.",
0xB09 => "Network Level Authentication is required, run RDPCheck as administrator.",
0x708 => "RDP is working, but the client doesn't allow loopback connections. Try to connect to your PC from another device in the network.",
_ => $"Unknown code 0x{discReason:X}"
};
if (discReason > 2)
{
MessageBox.Show(errStr, "Disconnected",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
// Restore registry
const string rdpTcpKey =
@"SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp";
try
{
RegistryHelper.WriteInt(rdpTcpKey, "SecurityLayer", _savedSecurityLayer);
RegistryHelper.WriteInt(rdpTcpKey, "UserAuthentication", _savedUserAuthentication);
}
catch { }
Application.Exit();
}
protected override void Dispose(bool disposing)
{
if (disposing) _rdp.Dispose();
base.Dispose(disposing);
}
}

@ -0,0 +1,15 @@
// Copyright 2026 sjackson0109 — Apache License 2.0
//
// RDPCheck — WinForms RDP tester entry point.
namespace RDPCheck;
internal static class Program
{
[STAThread]
internal static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
}
}

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<RootNamespace>RDPCheck</RootNamespace>
<AssemblyName>RDPCheck</AssemblyName>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>app.ico</ApplicationIcon>
<Description>WinForms RDP loopback connection tester — validates that RDP Wrapper is installed and accepting connections correctly.</Description>
</PropertyGroup>
<ItemGroup>
<Content Include="app.ico" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RDPWrap\RDPWrap.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,147 @@
// Copyright 2026 sjackson0109 — Apache License 2.0
//
// COM interop helpers for hosting the MsRdpClient2 ActiveX control and
// receiving its disconnection event without requiring pre-generated TLB wrappers.
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace RDPCheck;
// ── COM interfaces needed for connection-point event subscription ────────────
[ComImport]
[Guid("B196B284-BAB4-101A-B69C-00AA00341D07")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IConnectionPointContainer
{
void EnumConnectionPoints(out IntPtr ppEnum);
void FindConnectionPoint(ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IConnectionPoint ppCP);
}
[ComImport]
[Guid("B196B287-BAB4-101A-B69C-00AA00341D07")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IConnectionPoint
{
void GetConnectionInterface(out Guid pIID);
void GetConnectionPointContainer([MarshalAs(UnmanagedType.Interface)] out IConnectionPointContainer ppCPC);
void Advise([MarshalAs(UnmanagedType.Interface)] object pUnkSink, out uint pdwCookie);
void Unadvise(uint dwCookie);
void EnumConnections(out IntPtr ppEnum);
}
// ── IDispatch-based event interface (dispinterface) ──────────────────────────
// Matching the DIID of IMsTscAxEvents so the RDP control can route events here.
[ComVisible(true)]
[Guid("336D5562-EBA6-11D0-B0B0-00C04FD610D0")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
internal interface IRdpEvents
{
[DispId(1)] void OnConnecting();
[DispId(2)] void OnConnected();
[DispId(3)] void OnLoginComplete();
[DispId(4)] void OnDisconnected(int discReason);
// Additional high-DISPID events are ignored (not listed here)
}
// ── Managed event sink ────────────────────────────────────────────────────────
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
internal sealed class RdpEventSink : IRdpEvents
{
private readonly Action<int> _onDisconnected;
public RdpEventSink(Action<int> onDisconnected) => _onDisconnected = onDisconnected;
public void OnConnecting() { }
public void OnConnected() { }
public void OnLoginComplete() { }
public void OnDisconnected(int discReason) => _onDisconnected(discReason);
}
// ── AxHost wrapper for MsRdpClient2 ──────────────────────────────────────────
internal sealed class AxRdpClient2 : AxHost
{
// CLSID for MsRdpClient2 (msrdp.ocx / mstscax.dll)
private static readonly Guid Clsid = new("9059F30F-4EB1-4BD2-9FDC-36F43A218F4A");
// DIID for the default source interface (IMsTscAxEvents / DIMsTscAxEvents)
private static readonly Guid DiidEvents = new("336D5562-EBA6-11D0-B0B0-00C04FD610D0");
private dynamic? _ocx;
private IConnectionPoint? _cp;
private uint _cookie;
private RdpEventSink? _sink;
/// <summary>Raised on the UI thread when the RDP control disconnects.</summary>
public event EventHandler<int>? Disconnected;
public AxRdpClient2() : base(Clsid.ToString("B")) { }
protected override void AttachInterfaces()
{
_ocx = GetOcx() as dynamic;
}
/// <summary>
/// Wires up the COM event sink. Call once after the handle has been created
/// (i.e. after the form is shown).
/// </summary>
public void SubscribeEvents()
{
if (_ocx is null) return;
try
{
var cpContainer = (IConnectionPointContainer)_ocx;
var eventsIid = DiidEvents;
cpContainer.FindConnectionPoint(ref eventsIid, out var cp);
_sink = new RdpEventSink(reason => Invoke(
(Action)(() => Disconnected?.Invoke(this, reason))));
cp.Advise(_sink, out _cookie);
_cp = cp;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[RDP] SubscribeEvents failed: {ex.Message}");
}
}
/// <summary>Unsubscribes the event sink.</summary>
public void UnsubscribeEvents()
{
try { _cp?.Unadvise(_cookie); } catch { }
_cp = null;
}
// ── Typed property / method wrappers ────────────────────────────────────
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public string Server { set { if (_ocx != null) _ocx.Server = value; } }
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public string UserName { set { if (_ocx != null) _ocx.UserName = value; } }
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public string DisconnectedText { set { if (_ocx != null) _ocx.DisconnectedText = value; } }
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public string ConnectingText { set { if (_ocx != null) _ocx.ConnectingText = value; } }
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public string ConnectedStatusText { set { if (_ocx != null) _ocx.ConnectedStatusText = value; } }
public void SetPort(int port)
{
try { if (_ocx != null) _ocx.AdvancedSettings2.RDPPort = port; } catch { }
}
public void Connect()
{
try { _ocx?.Connect(); } catch { }
}
protected override void Dispose(bool disposing)
{
if (disposing) UnsubscribeEvents();
base.Dispose(disposing);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="RDPCheck.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
</application>
</compatibility>
</assembly>

@ -0,0 +1,10 @@
{
"version": 1,
"dependencies": {
"net10.0-windows7.0": {
"rdpwrap": {
"type": "Project"
}
}
}
}

@ -0,0 +1,53 @@
// Copyright 2026 sjackson0109 — Apache License 2.0
//
// LicenseForm — mirrors LicenseUnit.pas (TLicenseForm).
// Shows a readonly multiline license text with Accept / Decline buttons.
namespace RDPConf;
internal sealed class LicenseForm : Form
{
public LicenseForm(string licenseText)
{
Text = "License Agreement";
ClientSize = new Size(600, 440);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9f);
var mText = new TextBox
{
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Vertical,
Text = licenseText,
Location = new Point(8, 8),
Size = new Size(576, 380),
Font = new Font("Courier New", 8.5f),
BackColor = SystemColors.Window
};
var bAccept = new Button
{
Text = "Accept",
DialogResult = DialogResult.OK,
Location = new Point(428, 398),
Size = new Size(75, 26)
};
var bDecline = new Button
{
Text = "Decline",
DialogResult = DialogResult.Cancel,
Location = new Point(509, 398),
Size = new Size(75, 26)
};
AcceptButton = bAccept;
CancelButton = bDecline;
Controls.AddRange(new Control[] { mText, bAccept, bDecline });
}
}

@ -0,0 +1,540 @@
// Copyright 2026 sjackson0109 — Apache License 2.0
//
// All logic, registry keys, and label text match the Delphi original.
using System.Runtime.InteropServices;
using RDPWrap.Common;
namespace RDPConf;
internal sealed class MainForm : Form
{
// ── Controls ──────────────────────────────────────────────────────────────
private readonly CheckBox cbAllowTSConnections = new();
private readonly CheckBox cbSingleSessionPerUser = new();
private readonly CheckBox cbHideUsers = new();
private readonly CheckBox cbCustomPrg = new();
private readonly NumericUpDown seRDPPort = new();
private readonly Label lRDPPort = new();
private readonly GroupBox gbGeneral = new();
// NLA radio group
private readonly GroupBox gbNLA = new();
private readonly RadioButton rbNLA0 = new();
private readonly RadioButton rbNLA1 = new();
private readonly RadioButton rbNLA2 = new();
// Shadow radio group
private readonly GroupBox gbShadow = new();
private readonly RadioButton rbShadow0 = new();
private readonly RadioButton rbShadow1 = new();
private readonly RadioButton rbShadow2 = new();
private readonly RadioButton rbShadow3 = new();
private readonly RadioButton rbShadow4 = new();
// Diagnostics
private readonly GroupBox gbDiag = new();
private readonly Label lService = new();
private readonly Label lsService = new();
private readonly Label lListener = new();
private readonly Label lsListener = new();
private readonly Label lWrapper = new();
private readonly Label lsWrapper = new();
private readonly Label lTSVer = new();
private readonly Label lsTSVer = new();
private readonly Label lWrapVer = new();
private readonly Label lsWrapVer = new();
private readonly Label lsSuppVer = new();
// Buttons / Timer
private readonly Button bOK = new();
private readonly Button bCancel = new();
private readonly Button bApply = new();
private readonly Button bLicense = new();
private readonly System.Windows.Forms.Timer timer = new();
// State (mirrors Delphi globals)
private bool _ready;
private int _oldPort;
// ── Constructor ───────────────────────────────────────────────────────────
public MainForm()
{
SuspendLayout();
BuildLayout();
ResumeLayout(false);
PerformLayout();
// Wire events
cbAllowTSConnections.CheckedChanged += OnAnyChange;
cbSingleSessionPerUser.CheckedChanged += OnAnyChange;
cbHideUsers.CheckedChanged += OnAnyChange;
cbCustomPrg.CheckedChanged += OnAnyChange;
seRDPPort.ValueChanged += OnAnyChange;
rbNLA0.CheckedChanged += OnAnyChange;
rbNLA1.CheckedChanged += OnAnyChange;
rbNLA2.CheckedChanged += OnAnyChange;
rbShadow0.CheckedChanged += OnAnyChange;
rbShadow1.CheckedChanged += OnAnyChange;
rbShadow2.CheckedChanged += OnAnyChange;
rbShadow3.CheckedChanged += OnAnyChange;
rbShadow4.CheckedChanged += OnAnyChange;
bApply.Click += (_, _) => { WriteSettings(); bApply.Enabled = false; };
bOK.Click += (_, _) => { if (bApply.Enabled) { WriteSettings(); bApply.Enabled = false; } Close(); };
bCancel.Click += (_, _) => Close();
bLicense.Click += OnLicenseClick;
timer.Interval = 1000;
timer.Tick += TimerTimer;
FormClosing += OnFormClosing;
Shown += OnShown;
FormClosed += (_, _) => { if (ArchHelper.Is64Bit) ArchHelper.RevertWow64Redirection(); timer.Stop(); };
if (ArchHelper.Is64Bit) ArchHelper.DisableWow64Redirection();
ReadSettings();
_ready = true;
}
private void OnShown(object? sender, EventArgs e)
{
TimerTimer(sender, e); // immediate first tick
timer.Start();
}
// ── Layout ────────────────────────────────────────────────────────────────
private void BuildLayout()
{
Text = "RDP Wrapper Configuration";
ClientSize = new Size(540, 383);
FormBorderStyle = FormBorderStyle.FixedSingle;
MaximizeBox = false;
StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Segoe UI", 9f);
// ── General GroupBox ──────────────────────────────────────────────────
gbGeneral.Text = "General";
gbGeneral.Location = new Point(8, 8);
gbGeneral.Size = new Size(260, 175);
cbAllowTSConnections.Text = "Allow Remote Desktop connections";
cbAllowTSConnections.Location = new Point(10, 22);
cbAllowTSConnections.AutoSize = true;
cbSingleSessionPerUser.Text = "Single session per user";
cbSingleSessionPerUser.Location = new Point(10, 46);
cbSingleSessionPerUser.AutoSize = true;
cbHideUsers.Text = "Hide users on logon screen";
cbHideUsers.Location = new Point(10, 70);
cbHideUsers.AutoSize = true;
cbCustomPrg.Text = "Custom program support (HonorLegacySettings)";
cbCustomPrg.Location = new Point(10, 94);
cbCustomPrg.AutoSize = true;
lRDPPort.Text = "RDP Port:";
lRDPPort.Location = new Point(10, 124);
lRDPPort.AutoSize = true;
seRDPPort.Location = new Point(80, 121);
seRDPPort.Minimum = 1;
seRDPPort.Maximum = 65535;
seRDPPort.Value = 3389;
seRDPPort.Width = 70;
gbGeneral.Controls.AddRange(new Control[]
{
cbAllowTSConnections, cbSingleSessionPerUser, cbHideUsers,
cbCustomPrg, lRDPPort, seRDPPort
});
// ── NLA GroupBox ──────────────────────────────────────────────────────
gbNLA.Text = "Network Level Authentication";
gbNLA.Location = new Point(276, 8);
gbNLA.Size = new Size(254, 88);
rbNLA0.Text = "No NLA";
rbNLA0.Location = new Point(10, 20);
rbNLA0.AutoSize = true;
rbNLA1.Text = "Negotiate (server-side NLA)";
rbNLA1.Location = new Point(10, 42);
rbNLA1.AutoSize = true;
rbNLA2.Text = "Require NLA (client + server)";
rbNLA2.Location = new Point(10, 64);
rbNLA2.AutoSize = true;
gbNLA.Controls.AddRange(new Control[] { rbNLA0, rbNLA1, rbNLA2 });
// ── Shadow GroupBox ───────────────────────────────────────────────────
gbShadow.Text = "Shadowing";
gbShadow.Location = new Point(276, 100);
gbShadow.Size = new Size(254, 135);
rbShadow0.Text = "Disabled";
rbShadow0.Location = new Point(10, 20); rbShadow0.AutoSize = true;
rbShadow1.Text = "Full Access (with confirmation)";
rbShadow1.Location = new Point(10, 42); rbShadow1.AutoSize = true;
rbShadow2.Text = "Full Access (no confirmation)";
rbShadow2.Location = new Point(10, 62); rbShadow2.AutoSize = true;
rbShadow3.Text = "View Only (with confirmation)";
rbShadow3.Location = new Point(10, 82); rbShadow3.AutoSize = true;
rbShadow4.Text = "View Only (no confirmation)";
rbShadow4.Location = new Point(10, 102); rbShadow4.AutoSize = true;
gbShadow.Controls.AddRange(new Control[]
{ rbShadow0, rbShadow1, rbShadow2, rbShadow3, rbShadow4 });
// ── Diagnostics GroupBox ──────────────────────────────────────────────
gbDiag.Text = "Diagnostics";
gbDiag.Location = new Point(8, 190);
gbDiag.Size = new Size(522, 140);
AddDiagRow(gbDiag, "Service:", lService, lsService, new Point(10, 22));
AddDiagRow(gbDiag, "Listener:", lListener, lsListener, new Point(10, 44));
AddDiagRow(gbDiag, "Wrapper:", lWrapper, lsWrapper, new Point(10, 66));
AddDiagRow(gbDiag, "TS version:", lTSVer, lsTSVer, new Point(10, 88));
AddDiagRow(gbDiag, "Wrapper version:", lWrapVer, lsWrapVer, new Point(10, 110));
lsSuppVer.Location = new Point(280, 88);
lsSuppVer.AutoSize = true;
lsSuppVer.Visible = false;
gbDiag.Controls.Add(lsSuppVer);
// ── Buttons ───────────────────────────────────────────────────────────
bLicense.Text = "License";
bLicense.Size = new Size(80, 26);
bLicense.Location = new Point(8, 345);
bApply.Text = "Apply";
bApply.Enabled = false;
bApply.Size = new Size(80, 26);
bApply.Location = new Point(276, 345);
bOK.Text = "OK";
bOK.Size = new Size(80, 26);
bOK.Location = new Point(364, 345);
bCancel.Text = "Cancel";
bCancel.Size = new Size(80, 26);
bCancel.Location = new Point(452, 345);
Controls.AddRange(new Control[]
{
gbGeneral, gbNLA, gbShadow, gbDiag,
bLicense, bApply, bOK, bCancel
});
}
private static void AddDiagRow(GroupBox parent, string labelText,
Label lbl, Label status, Point origin)
{
lbl.Text = labelText;
lbl.Location = new Point(origin.X, origin.Y);
lbl.AutoSize = true;
status.Text = "...";
status.Location = new Point(origin.X + 120, origin.Y);
status.AutoSize = true;
status.ForeColor = SystemColors.GrayText;
parent.Controls.Add(lbl);
parent.Controls.Add(status);
}
private void OnAnyChange(object? sender, EventArgs e)
{
if (_ready) bApply.Enabled = true;
}
// ── ReadSettings (mirrors Delphi ReadSettings) ────────────────────────────
private void ReadSettings()
{
const string tsKey = @"SYSTEM\CurrentControlSet\Control\Terminal Server";
const string rdpTcp = @"SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp";
const string policies = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System";
cbAllowTSConnections.Checked = !RegistryHelper.ReadBool(tsKey, "fDenyTSConnections", false);
cbSingleSessionPerUser.Checked = RegistryHelper.ReadBool(tsKey, "fSingleSessionPerUser", false);
cbCustomPrg.Checked = RegistryHelper.ReadBool(tsKey, "HonorLegacySettings", false);
int port = RegistryHelper.ReadInt(rdpTcp, "PortNumber", 3389);
seRDPPort.Value = Math.Clamp(port, 1, 65535);
_oldPort = port;
int secLayer = RegistryHelper.ReadInt(rdpTcp, "SecurityLayer", 0);
int userAuth = RegistryHelper.ReadInt(rdpTcp, "UserAuthentication", 0);
int shadow = RegistryHelper.ReadInt(rdpTcp, "Shadow", -1);
_ = (secLayer, userAuth) switch
{
(0, 0) => rbNLA0.Checked = true,
(1, 0) => rbNLA1.Checked = true,
(2, 1) => rbNLA2.Checked = true,
_ => rbNLA0.Checked = true
};
SetShadowRadio(shadow);
cbHideUsers.Checked = RegistryHelper.ReadBool(policies, "dontdisplaylastusername", false);
}
private void SetShadowRadio(int idx)
{
RadioButton[] rbs = { rbShadow0, rbShadow1, rbShadow2, rbShadow3, rbShadow4 };
if (idx >= 0 && idx < rbs.Length)
rbs[idx].Checked = true;
}
private int GetShadowIndex()
{
RadioButton[] rbs = { rbShadow0, rbShadow1, rbShadow2, rbShadow3, rbShadow4 };
for (int i = 0; i < rbs.Length; i++)
if (rbs[i].Checked) return i;
return -1;
}
// ── WriteSettings (mirrors Delphi WriteSettings) ──────────────────────────
private void WriteSettings()
{
const string tsKey = @"SYSTEM\CurrentControlSet\Control\Terminal Server";
const string rdpTcp = @"SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp";
const string policiesRdp = @"SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services";
const string policies = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System";
RegistryHelper.WriteBool(tsKey, "fDenyTSConnections", !cbAllowTSConnections.Checked);
RegistryHelper.WriteBool(tsKey, "fSingleSessionPerUser", cbSingleSessionPerUser.Checked);
RegistryHelper.WriteBool(tsKey, "HonorLegacySettings", cbCustomPrg.Checked);
int newPort = (int)seRDPPort.Value;
RegistryHelper.WriteInt(rdpTcp, "PortNumber", newPort);
if (_oldPort != newPort)
{
_oldPort = newPort;
ProcessHelper.ExecWait(
$"netsh advfirewall firewall set rule name=\"Remote Desktop\" new localport={newPort}");
}
// NLA
(int sl, int ua) = (rbNLA0.Checked, rbNLA1.Checked, rbNLA2.Checked) switch
{
(true, _, _ ) => (0, 0),
(_, true, _ ) => (1, 0),
(_, _, true) => (2, 1),
_ => (-1, -1)
};
if (sl >= 0)
{
RegistryHelper.WriteInt(rdpTcp, "SecurityLayer", sl);
RegistryHelper.WriteInt(rdpTcp, "UserAuthentication", ua);
}
// Shadow
int shadowIdx = GetShadowIndex();
if (shadowIdx >= 0)
{
RegistryHelper.WriteInt(rdpTcp, "Shadow", shadowIdx);
RegistryHelper.WriteInt(policiesRdp, "Shadow", shadowIdx);
}
RegistryHelper.WriteBool(policies, "dontdisplaylastusername", cbHideUsers.Checked);
}
// ── TimerTimer (mirrors Delphi TimerTimer) ────────────────────────────────
private void TimerTimer(object? sender, EventArgs e)
{
// ── Wrapper state ──
string wrapperPath = string.Empty;
int wrapState = IsWrapperInstalled(ref wrapperPath);
bool checkSupp = false;
string iniPath = string.Empty;
switch (wrapState)
{
case -1:
SetStatus(lsWrapper, "Unknown", SystemColors.GrayText);
break;
case 0:
SetStatus(lsWrapper, "Not installed", SystemColors.GrayText);
break;
case 1:
SetStatus(lsWrapper, "Installed", Color.Green);
iniPath = Path.Combine(
Path.GetDirectoryName(ArchHelper.ExpandPath(wrapperPath))!,
"rdpwrap.ini");
checkSupp = File.Exists(iniPath);
break;
case 2:
SetStatus(lsWrapper, "3rd-party", Color.Red);
break;
}
// ── Service state ──
int svcState = GetTermSrvState();
// dwCurrentState constants (mirrors NativeMethods SERVICE_* constants)
string svcText = svcState switch
{
1 => "Stopped",
2 => "Starting...",
3 => "Stopping...",
4 => "Running",
5 => "Resuming...",
6 => "Suspending...",
7 => "Suspended",
_ => "Unknown"
};
Color svcColor = svcState == 4 ? Color.Green // SERVICE_RUNNING
: svcState == 1 ? Color.Red // SERVICE_STOPPED
: SystemColors.GrayText;
SetStatus(lsService, svcText, svcColor);
// ── Listener ──
bool listening = IsListenerWorking();
SetStatus(lsListener,
listening ? "Listening" : "Not listening",
listening ? Color.Green : Color.Red);
// ── Wrapper version ──
string wrapExp = ArchHelper.ExpandPath(wrapperPath);
var wrapVer = string.IsNullOrEmpty(wrapperPath) ? null
: FileVersionHelper.GetVersion(wrapExp);
if (wrapVer is null)
SetStatus(lsWrapVer, "N/A", SystemColors.GrayText);
else
SetStatus(lsWrapVer, wrapVer.ToString(), SystemColors.WindowText);
// ── TS version ──
var tsVer = FileVersionHelper.GetVersionExpanded(@"%SystemRoot%\System32\termsrv.dll");
if (tsVer is null)
{
SetStatus(lsTSVer, "N/A", SystemColors.GrayText);
lsSuppVer.Visible = false;
return;
}
SetStatus(lsTSVer, tsVer.ToString(), SystemColors.WindowText);
lsSuppVer.Visible = checkSupp;
if (checkSupp)
{
string iniContent = IniHelper.LoadText(iniPath);
int level = IniHelper.CheckSupportLevel(iniContent, tsVer);
(string suppText, Color suppColor) = level switch
{
0 => ("[not supported]", Color.Red),
1 => ("[supported partially]", Color.Olive),
_ => ("[fully supported]", Color.Green)
};
lsSuppVer.Text = suppText;
lsSuppVer.ForeColor = suppColor;
}
}
// ── Helper: IsWrapperInstalled ────────────────────────────────────────────
/// <returns>-1=error, 0=not installed, 1=installed, 2=3rd-party</returns>
private static int IsWrapperInstalled(ref string wrapperPath)
{
wrapperPath = string.Empty;
var host = RegistryHelper.ReadString(
@"SYSTEM\CurrentControlSet\Services\TermService", "ImagePath") ?? string.Empty;
if (!host.Contains("svchost.exe", StringComparison.OrdinalIgnoreCase)) return 2;
var svcDll = RegistryHelper.ReadString(
@"SYSTEM\CurrentControlSet\Services\TermService\Parameters", "ServiceDll") ?? string.Empty;
if (svcDll.Length == 0) return -1;
if (!svcDll.Contains("termsrv.dll", StringComparison.OrdinalIgnoreCase) &&
!svcDll.Contains("rdpwrap.dll", StringComparison.OrdinalIgnoreCase))
return 2;
if (svcDll.Contains("rdpwrap.dll", StringComparison.OrdinalIgnoreCase))
{
wrapperPath = svcDll;
return 1;
}
return 0;
}
// ── Helper: GetTermSrvState (via SCM) ─────────────────────────────────────
private static int GetTermSrvState()
=> ServiceHelper.GetCurrentState("TermService");
// ── Helper: IsListenerWorking (via WinStationEnumerateW) ──────────────────
private static bool IsListenerWorking()
{
if (!NativeMethods.WinStationEnumerate(IntPtr.Zero,
out IntPtr pSessions, out uint count)) return false;
bool found = false;
try
{
// Each entry is { DWORD SessionId; WCHAR[34] Name; DWORD State }
// = 4 + 68 + 4 = 76 bytes on x64 (with natural alignment the struct is 76 bytes)
const int stride = 76; // sizeof(WTS_SESSION_INFOW) — matches Delphi packed array
for (uint i = 0; i < count; i++)
{
IntPtr entry = pSessions + (int)(i * stride);
// Name is at offset 4, WCHAR[34]
string name = Marshal.PtrToStringUni(entry + 4, 34).TrimEnd('\0');
if (name.Equals("RDP-Tcp", StringComparison.Ordinal))
{
found = true;
break;
}
}
}
finally
{
NativeMethods.WinStationFreeMemory(pSessions);
}
return found;
}
// ── Helper: SetStatus ─────────────────────────────────────────────────────
private static void SetStatus(Label lbl, string text, Color color)
{
lbl.Text = text;
lbl.ForeColor = color;
}
// ── OnFormClosing — unsaved-changes guard ─────────────────────────────────
private void OnFormClosing(object? sender, FormClosingEventArgs e)
{
if (bApply.Enabled)
{
var result = MessageBox.Show(
"Settings are not saved. Do you want to exit?",
"Warning",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
e.Cancel = result != DialogResult.Yes;
}
}
// ── License button ────────────────────────────────────────────────────────
private void OnLicenseClick(object? sender, EventArgs e)
{
var text = ResourceHelper.ReadText(
"RDPConf.Resources.license.txt",
System.Reflection.Assembly.GetExecutingAssembly());
text ??= ResourceHelper.ReadText("RDPWInst.Resources.license.txt") ?? "(license not found)";
using var dlg = new LicenseForm(text);
if (dlg.ShowDialog(this) != DialogResult.OK)
Application.Exit();
}
}

@ -0,0 +1,15 @@
// Copyright 2026 sjackson0109 — Apache License 2.0
//
// RDPConf — WinForms configuration GUI entry point.
namespace RDPConf;
internal static class Program
{
[STAThread]
internal static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
}
}

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<RootNamespace>RDPConf</RootNamespace>
<AssemblyName>RDPConf</AssemblyName>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>app.ico</ApplicationIcon>
<Description>WinForms configuration GUI for RDP Wrapper — manage concurrent session limits, RDP port, and security settings.</Description>
</PropertyGroup>
<ItemGroup>
<Content Include="app.ico" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RDPWrap\RDPWrap.csproj" />
</ItemGroup>
<!-- Optional embedded license text (copy from msi/rdpwrap.ini or assets before build) -->
<ItemGroup>
<EmbeddedResource Include="Resources\license.txt" Condition="Exists('Resources\license.txt')" />
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="RDPConf.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
</application>
</compatibility>
</assembly>

@ -0,0 +1,10 @@
{
"version": 1,
"dependencies": {
"net10.0-windows7.0": {
"rdpwrap": {
"type": "Project"
}
}
}
}

@ -0,0 +1 @@
Subproject commit 68da37acab6593c329776644944f55695a131731

@ -0,0 +1,744 @@
// Copyright 2026 sjackson0109 — Apache License 2.0
//
// InstallerEngine — translates every procedure in RDPWInst.dpr to C#.
using System.Reflection;
using RDPWrap.Common;
namespace RDPWInst;
/// <summary>
/// Orchestrates install / uninstall / update / restart of the RDP Wrapper.
/// All public methods return an exit code (0 = success).
/// </summary>
internal sealed class InstallerEngine
{
// ── State (mirrors Delphi globals) ────────────────────────────────────────
private bool _installed;
private bool _online;
private string _wrapPath = string.Empty;
private string _termServicePath = string.Empty;
private string _termSrvVerTxt = string.Empty;
private uint _termServicePid;
private string[] _shareServices = Array.Empty<string>();
private const string TermService = "TermService";
// Latest release download base URL
private const string ReleaseBaseUrl =
"https://github.com/sjackson0109/rdpwrap/releases/latest/download/";
// ── Public entry points ───────────────────────────────────────────────────
/// <summary>
/// Install the wrapper. Mirrors the <c>-i</c> branch in RDPWInst.dpr.
/// </summary>
public int Install(bool toSystem32, bool online, bool force = false)
{
if (_installed)
{
if (!force)
{
Console.WriteLine("[*] RDP Wrapper Library is already installed.");
return unchecked((int)NativeMethods.ERROR_ACCESS_DENIED);
}
Console.WriteLine("[*] Existing installation detected -- uninstalling first (force mode)...");
int urc = Uninstall(keepSettings: true);
if (urc != 0)
{
Console.Error.WriteLine($"[-] Force-uninstall failed (code {urc}) -- aborting install.");
return urc;
}
// Refresh state: after a successful uninstall _installed should now be false.
CheckInstall();
}
Console.WriteLine("[*] Notice to user:");
Console.WriteLine(" - By using all or any portion of this software, you are agreeing");
Console.WriteLine(" to be bound by all the terms and conditions of the license agreement.");
Console.WriteLine(" - To read the license agreement, run the installer with -l parameter.");
Console.WriteLine(" - If you do not agree to any terms of the license agreement,");
Console.WriteLine(" do not use the software.");
Console.WriteLine("[*] Installing...");
_wrapPath = toSystem32
? @"%SystemRoot%\system32\rdpwrap.dll"
: @"%ProgramFiles%\RDP Wrapper\rdpwrap.dll";
if (ArchHelper.Is64Bit) ArchHelper.DisableWow64Redirection();
CheckTermsrvVersion();
CheckTermsrvProcess();
Console.WriteLine("[*] Extracting files...");
_online = online;
ExtractFiles();
Console.WriteLine("[*] Checking INI coverage for installed termsrv.dll version...");
TryAutoGenerateOffsets();
Console.WriteLine("[*] Configuring service library...");
SetWrapperDll();
Console.WriteLine("[*] Checking dependencies...");
CheckTermsrvDependencies();
Console.WriteLine("[*] Terminating service...");
SecurityHelper.AddPrivilege(NativeMethods.SE_DEBUG_NAME);
ProcessHelper.KillProcess(_termServicePid);
Thread.Sleep(1000);
RestartSharedServices();
Thread.Sleep(500);
ServiceHelper.StartService(TermService);
Thread.Sleep(500);
Console.WriteLine("[*] Configuring registry...");
TSConfigRegistry(enable: true);
Console.WriteLine("[*] Configuring firewall...");
TSConfigFirewall(enable: true);
Console.WriteLine("[+] Successfully installed.");
if (ArchHelper.Is64Bit) ArchHelper.RevertWow64Redirection();
return 0;
}
/// <summary>
/// Uninstall the wrapper. Mirrors the <c>-u</c> branch.
/// </summary>
public int Uninstall(bool keepSettings)
{
if (!_installed)
{
Console.WriteLine("[*] RDP Wrapper Library is not installed.");
return unchecked((int)NativeMethods.ERROR_ACCESS_DENIED);
}
Console.WriteLine("[*] Uninstalling...");
if (ArchHelper.Is64Bit) ArchHelper.DisableWow64Redirection();
CheckTermsrvProcess();
Console.WriteLine("[*] Resetting service library...");
ResetServiceDll();
Console.WriteLine("[*] Terminating service...");
SecurityHelper.AddPrivilege(NativeMethods.SE_DEBUG_NAME);
ProcessHelper.KillProcess(_termServicePid);
Thread.Sleep(1000);
Console.WriteLine("[*] Removing files...");
DeleteFiles();
RestartSharedServices();
Thread.Sleep(500);
ServiceHelper.StartService(TermService);
Thread.Sleep(500);
if (!keepSettings)
{
Console.WriteLine("[*] Configuring registry...");
TSConfigRegistry(enable: false);
Console.WriteLine("[*] Configuring firewall...");
TSConfigFirewall(enable: false);
}
if (ArchHelper.Is64Bit) ArchHelper.RevertWow64Redirection();
Console.WriteLine("[+] Successfully uninstalled.");
return 0;
}
/// <summary>
/// Download the latest rdpwrap.ini. Mirrors the <c>-w</c> / CheckUpdate branch.
/// </summary>
public int Update()
{
if (!_installed)
{
Console.WriteLine("[*] RDP Wrapper Library is not installed.");
return unchecked((int)NativeMethods.ERROR_ACCESS_DENIED);
}
Console.WriteLine("[*] Checking for updates...");
return CheckUpdate();
}
/// <summary>
/// Force-restart Terminal Services. Mirrors the <c>-r</c> branch.
/// </summary>
public int Restart()
{
Console.WriteLine("[*] Restarting...");
CheckTermsrvProcess();
Console.WriteLine("[*] Terminating service...");
SecurityHelper.AddPrivilege(NativeMethods.SE_DEBUG_NAME);
ProcessHelper.KillProcess(_termServicePid);
Thread.Sleep(1000);
RestartSharedServices();
Thread.Sleep(500);
ServiceHelper.StartService(TermService);
Console.WriteLine("[+] Done.");
return 0;
}
// ── CheckInstall ──────────────────────────────────────────────────────────
/// <summary>
/// Validates the TermService registry image path and sets
/// <see cref="_installed"/> and <see cref="_termServicePath"/>.
/// Mirrors the Delphi CheckInstall procedure.
/// </summary>
public void CheckInstall()
{
const string svcKey = @"SYSTEM\CurrentControlSet\Services\TermService";
const string paramsKey = @"SYSTEM\CurrentControlSet\Services\TermService\Parameters";
var imagePath = RegistryHelper.ReadString(svcKey, "ImagePath") ?? string.Empty;
if (!imagePath.Contains("svchost.exe", StringComparison.OrdinalIgnoreCase) &&
!imagePath.Contains("svchost -k", StringComparison.OrdinalIgnoreCase))
{
Console.Error.WriteLine("[-] TermService is hosted in a custom application (BeTwin, etc.) - unsupported.");
Console.Error.WriteLine($"[*] ImagePath: \"{imagePath}\".");
Environment.Exit(unchecked((int)NativeMethods.ERROR_NOT_SUPPORTED));
}
var serviceDll = RegistryHelper.ReadString(paramsKey, "ServiceDll") ?? string.Empty;
if (!serviceDll.Contains("termsrv.dll", StringComparison.OrdinalIgnoreCase) &&
!serviceDll.Contains("rdpwrap.dll", StringComparison.OrdinalIgnoreCase))
{
Console.Error.WriteLine("[-] Another third-party TermService library is installed.");
Console.Error.WriteLine($"[*] ServiceDll: \"{serviceDll}\".");
Environment.Exit(unchecked((int)NativeMethods.ERROR_NOT_SUPPORTED));
}
_termServicePath = serviceDll;
_installed = serviceDll.Contains("rdpwrap.dll", StringComparison.OrdinalIgnoreCase);
}
// ── CheckTermsrvProcess ────────────────────────────────────────────────────
/// <summary>
/// Finds the TermService process ID, auto-starts the service if needed,
/// and collects co-hosted service names. Mirrors CheckTermsrvProcess.
/// </summary>
private void CheckTermsrvProcess()
{
bool started = false;
retry:
var services = ServiceHelper.EnumServiceProcesses();
var ts = services.FirstOrDefault(s =>
s.ServiceName.Equals(TermService, StringComparison.OrdinalIgnoreCase));
if (ts is null)
{
Console.Error.WriteLine($"[-] {TermService} not found.");
Environment.Exit(unchecked((int)NativeMethods.ERROR_SERVICE_DOES_NOT_EXIST));
return;
}
if (ts.ProcessId == 0)
{
if (started)
{
Console.Error.WriteLine("[-] Failed to set up TermService. Unknown error.");
Environment.Exit(unchecked((int)NativeMethods.ERROR_SERVICE_NOT_ACTIVE));
return;
}
ServiceHelper.SetStartType(TermService, NativeMethods.SERVICE_AUTO_START);
ServiceHelper.StartService(TermService);
started = true;
goto retry;
}
_termServicePid = ts.ProcessId;
Console.WriteLine($"[+] TermService found (pid {_termServicePid}).");
_shareServices = services
.Where(s => s.ProcessId == _termServicePid &&
!s.ServiceName.Equals(TermService, StringComparison.OrdinalIgnoreCase))
.Select(s => s.ServiceName)
.ToArray();
if (_shareServices.Length > 0)
Console.WriteLine($"[*] Shared services found: {string.Join(", ", _shareServices)}");
else
Console.WriteLine("[*] No shared services found.");
}
// ── CheckTermsrvDependencies ───────────────────────────────────────────────
/// <summary>
/// Ensures CertPropSvc and SessionEnv are not disabled.
/// Mirrors the Delphi CheckTermsrvDependencies procedure.
/// </summary>
private static void CheckTermsrvDependencies()
{
foreach (var svc in new[] { "CertPropSvc", "SessionEnv" })
{
if (ServiceHelper.GetStartType(svc) == (int)NativeMethods.SERVICE_DISABLED)
ServiceHelper.SetStartType(svc, NativeMethods.SERVICE_DEMAND_START);
}
}
// ── CheckTermsrvVersion ────────────────────────────────────────────────────
/// <summary>
/// Reads the termsrv.dll version and classifies support level.
/// Mirrors the Delphi CheckTermsrvVersion procedure.
/// </summary>
private void CheckTermsrvVersion()
{
var fv = FileVersionHelper.GetVersionExpanded(_termServicePath);
if (fv is null)
{
Console.Error.WriteLine("[-] Could not read termsrv.dll version.");
return;
}
_termSrvVerTxt = fv.ToString();
Console.WriteLine($"[*] Terminal Services version: {_termSrvVerTxt}");
// Unsupported legacy versions
if (fv.Major == 5)
{
var label = (ArchHelper.Arch == 32) ? "x86" : "x64";
Console.WriteLine($"[!] Windows XP / Server 2003 ({label}) is not supported.");
return;
}
// Load the built-in INI to check support level
var builtInIni = ResourceHelper.ReadText(
"RDPWInst.Resources.rdpwrap.ini",
Assembly.GetExecutingAssembly()) ?? string.Empty;
int level = IniHelper.CheckSupportLevel(builtInIni, fv);
switch (level)
{
case 0:
Console.WriteLine("[-] This version of Terminal Services is not supported.");
Console.WriteLine("Try running \"update.bat\" or \"RDPWInst -w\" to download latest INI file.");
break;
case 1:
Console.WriteLine("[!] This version of Terminal Services is supported partially.");
Console.WriteLine("It means you may have some limitations such as only 2 concurrent sessions.");
Console.WriteLine("Try running \"update.bat\" or \"RDPWInst -w\" to download latest INI file.");
break;
case 2:
Console.WriteLine("[+] This version of Terminal Services is fully supported.");
break;
}
}
// ── TSConfigRegistry ──────────────────────────────────────────────────────
/// <summary>
/// Writes (or clears) the TS-enable registry values.
/// Mirrors the Delphi TSConfigRegistry procedure.
/// </summary>
private static void TSConfigRegistry(bool enable)
{
const string tsKey = @"SYSTEM\CurrentControlSet\Control\Terminal Server";
const string licKey = @"SYSTEM\CurrentControlSet\Control\Terminal Server\Licensing Core";
const string winlogon = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon";
const string addInsBase = @"SYSTEM\CurrentControlSet\Control\Terminal Server\AddIns";
RegistryHelper.WriteBool(tsKey, "fDenyTSConnections", !enable);
if (enable)
{
RegistryHelper.WriteBool(licKey, "EnableConcurrentSessions", true);
RegistryHelper.WriteBool(winlogon, "AllowMultipleTSSessions", true);
RegistryHelper.WriteBool(tsKey, "AllowRemoteRPC", true);
RegistryHelper.WriteBool(tsKey, "EnableLinkedConnections", true);
// AddIns sub-keys (only create if the parent key is absent)
if (Microsoft.Win32.Registry.LocalMachine.OpenSubKey(addInsBase) is null)
{
RegistryHelper.WriteString(addInsBase + @"\Clip Redirector", "Name", "RDPClip");
RegistryHelper.WriteInt (addInsBase + @"\Clip Redirector", "Type", 3);
RegistryHelper.WriteString(addInsBase + @"\DND Redirector", "Name", "RDPDND");
RegistryHelper.WriteInt (addInsBase + @"\DND Redirector", "Type", 3);
RegistryHelper.WriteInt (addInsBase + @"\Dynamic VC", "Type", -1);
}
}
}
// ── TSConfigFirewall ──────────────────────────────────────────────────────
private static void TSConfigFirewall(bool enable)
{
if (enable)
{
ProcessHelper.ExecWait(
"netsh advfirewall firewall add rule name=\"Remote Desktop\" " +
"dir=in protocol=tcp localport=3389 profile=any action=allow");
ProcessHelper.ExecWait(
"netsh advfirewall firewall add rule name=\"Remote Desktop\" " +
"dir=in protocol=udp localport=3389 profile=any action=allow");
}
else
{
ProcessHelper.ExecWait(
"netsh advfirewall firewall delete rule name=\"Remote Desktop\"");
}
}
// ── ExtractFiles ──────────────────────────────────────────────────────────
/// <summary>
/// Creates the install directory, sets ACLs, downloads or extracts the
/// INI file, and extracts the correct rdpwrap DLL + optional helpers.
/// Mirrors the Delphi ExtractFiles procedure.
/// </summary>
private void ExtractFiles()
{
var asm = Assembly.GetExecutingAssembly();
var fullPath = ArchHelper.ExpandPath(_wrapPath);
var dir = Path.GetDirectoryName(fullPath)!;
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
Console.WriteLine($"[+] Folder created: {dir}");
SecurityHelper.GrantSidFullAccess(dir, "S-1-5-18"); // Local System
SecurityHelper.GrantSidFullAccess(dir, "S-1-5-6"); // Service group
}
// ── INI file ──
var iniPath = Path.Combine(dir, "rdpwrap.ini");
if (_online)
{
Console.WriteLine("[*] Downloading latest INI file...");
var content = HttpHelper.DownloadString(ReleaseBaseUrl + "rdpwrap.ini");
if (content is not null)
{
File.WriteAllText(iniPath, content, System.Text.Encoding.UTF8);
Console.WriteLine($"[+] Latest INI file -> {iniPath}");
}
else
{
Console.WriteLine("[-] Failed to get online INI file, using built-in.");
_online = false;
}
}
if (!_online)
{
// Try a local rdpwrap.ini beside the installer first
var localIni = Path.Combine(
Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? ".",
"rdpwrap.ini");
if (File.Exists(localIni))
{
File.Copy(localIni, iniPath, overwrite: true);
Console.WriteLine($"[+] Current INI file -> {iniPath}");
}
else
{
ResourceHelper.ExtractToDisk("RDPWInst.Resources.rdpwrap.ini", iniPath, asm);
}
}
// ── Core DLL ──
var dllRes = ArchHelper.Is64Bit ? "RDPWInst.Resources.rdpw64.dll"
: "RDPWInst.Resources.rdpw32.dll";
ResourceHelper.ExtractToDisk(dllRes, fullPath, asm);
// ── Optional helpers (Vista / Win7 clipboard redirect, Win10 RFX codec) ──
ExtractOptionalHelper(asm, dir);
}
private void ExtractOptionalHelper(Assembly asm, string dir)
{
var fv = FileVersionHelper.GetVersionExpanded(_termServicePath);
if (fv is null) return;
var arch = ArchHelper.Is64Bit ? "64" : "32";
// rdpclip: Vista 6.0 and Win7 6.1
string? clipRes = (fv.Major, fv.Minor) switch
{
(6, 0) => $"RDPWInst.Resources.rdpclip60{arch}.exe",
(6, 1) => $"RDPWInst.Resources.rdpclip61{arch}.exe",
_ => null
};
if (clipRes is not null)
{
var dest = ArchHelper.ExpandPath(@"%SystemRoot%\System32\rdpclip.exe");
if (!File.Exists(dest))
ResourceHelper.ExtractToDisk(clipRes, dest, asm);
}
// rfxvmt.dll: Windows 10 (6.10.x maps to 10.0 in NT versioning)
if (fv.Major == 10 && fv.Minor == 0)
{
var rfxRes = $"RDPWInst.Resources.rfxvmt{arch}.dll";
var rfxDest = ArchHelper.ExpandPath(@"%SystemRoot%\System32\rfxvmt.dll");
if (!File.Exists(rfxDest))
ResourceHelper.ExtractToDisk(rfxRes, rfxDest, asm);
}
}
// ── SetWrapperDll / ResetServiceDll ──────────────────────────────────────
private void SetWrapperDll()
{
const string key = @"SYSTEM\CurrentControlSet\Services\TermService\Parameters";
RegistryHelper.WriteExpandString(key, "ServiceDll", _wrapPath);
// Vista 6.0 workaround — reg.exe write to bypass WOW64 quirk
var fv = FileVersionHelper.GetVersionExpanded(_termServicePath);
if (fv is { Major: 6, Minor: 0 } && ArchHelper.Is64Bit)
{
ProcessHelper.ExecWait(
$"\"{ArchHelper.ExpandPath("%SystemRoot%")}\\system32\\reg.exe\" " +
$"add HKLM\\SYSTEM\\CurrentControlSet\\Services\\TermService\\Parameters " +
$"/v ServiceDll /t REG_EXPAND_SZ /d \"{_wrapPath}\" /f");
}
}
private static void ResetServiceDll()
{
const string key = @"SYSTEM\CurrentControlSet\Services\TermService\Parameters";
RegistryHelper.WriteExpandString(key, "ServiceDll",
@"%SystemRoot%\System32\termsrv.dll");
}
// ── DeleteFiles ───────────────────────────────────────────────────────────
private void DeleteFiles()
{
var fullPath = ArchHelper.ExpandPath(_termServicePath);
var dir = Path.GetDirectoryName(fullPath)!;
var iniPath = Path.Combine(dir, "rdpwrap.ini");
TryDelete(iniPath);
TryDelete(fullPath);
TryRemoveDir(dir);
}
private static void TryDelete(string path)
{
try
{
File.Delete(path);
Console.WriteLine($"[+] Removed file: {path}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[-] DeleteFile error: {ex.Message}");
}
}
private static void TryRemoveDir(string dir)
{
try
{
Directory.Delete(dir);
Console.WriteLine($"[+] Removed folder: {dir}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[-] RemoveDirectory error: {ex.Message}");
}
}
// ── TryAutoGenerateOffsets ────────────────────────────────────────────────
/// <summary>
/// If the running termsrv.dll version is absent from rdpwrap.ini, downloads
/// RDPWrapOffsetFinder + Zydis from the release assets and runs the finder
/// to append generated offsets. Mirrors the Delphi TryAutoGenerateOffsets.
/// </summary>
private void TryAutoGenerateOffsets()
{
if (string.IsNullOrEmpty(_termSrvVerTxt)) return;
var fullPath = ArchHelper.ExpandPath(_wrapPath);
var iniPath = Path.Combine(Path.GetDirectoryName(fullPath)!, "rdpwrap.ini");
if (IniHelper.HasSection(iniPath, _termSrvVerTxt))
{
Console.WriteLine($"[+] Version {_termSrvVerTxt} is covered in INI.");
return;
}
Console.WriteLine($"[!] Version {_termSrvVerTxt} not found in INI.");
Console.WriteLine("[*] Attempting automatic offset generation via RDPWrapOffsetFinder...");
var archSuffix = ArchHelper.Is64Bit ? "_x64" : "_x86";
var tempDir = Path.Combine(Path.GetTempPath(), "rdpwrapoffset");
try { Directory.CreateDirectory(tempDir); }
catch
{
Console.Error.WriteLine("[-] Could not create temp directory. Skipping auto-generation.");
return;
}
var exePath = Path.Combine(tempDir, "RDPWrapOffsetFinder.exe");
var dllPath = Path.Combine(tempDir, "Zydis.dll");
Console.WriteLine($"[*] Downloading RDPWrapOffsetFinder{archSuffix}.exe ...");
if (!HttpHelper.DownloadFile(ReleaseBaseUrl + $"RDPWrapOffsetFinder{archSuffix}.exe", exePath))
{
Console.Error.WriteLine("[-] Download failed. The release asset may not yet be published.");
Console.Error.WriteLine("[!] Run the publish-ini workflow on the sjackson0109/rdpwrap repository,");
Console.Error.WriteLine("[!] then re-run this installer to enable auto-generation.");
return;
}
Console.WriteLine($"[*] Downloading Zydis{archSuffix}.dll ...");
if (!HttpHelper.DownloadFile(ReleaseBaseUrl + $"Zydis{archSuffix}.dll", dllPath))
{
Console.Error.WriteLine("[-] Zydis download failed. Skipping auto-generation.");
File.Delete(exePath);
return;
}
Console.WriteLine($"[*] Running offset finder for termsrv.dll {_termSrvVerTxt} ...");
// Run via cmd.exe so that >> redirect to the INI file functions correctly
var sysCmd = ArchHelper.ExpandPath(@"%SystemRoot%\System32\cmd.exe");
ProcessHelper.ExecWait($"\"{sysCmd}\" /c \"\"{exePath}\" >> \"{iniPath}\"\"");
if (IniHelper.HasSection(iniPath, _termSrvVerTxt))
Console.WriteLine($"[+] Offsets generated successfully for version {_termSrvVerTxt}");
else
Console.WriteLine($"[!] Offset finder ran but [{_termSrvVerTxt}] was not added. " +
"Session may be limited or unstable for this build.");
// Clean up temporary tool files
try
{
File.Delete(exePath);
File.Delete(dllPath);
Directory.Delete(tempDir);
}
catch { /* best-effort */ }
}
// ── CheckUpdate (GitINIFile path) ─────────────────────────────────────────
private int CheckUpdate()
{
var fullPath = ArchHelper.ExpandPath(_termServicePath);
var iniPath = Path.Combine(Path.GetDirectoryName(fullPath)!, "rdpwrap.ini");
if (!TryGetIniDate(iniPath, null, out int oldDate))
return unchecked((int)NativeMethods.ERROR_ACCESS_DENIED);
Console.WriteLine($"[*] Current update date: {FormatDate(oldDate)}");
var latest = HttpHelper.DownloadString(ReleaseBaseUrl + "rdpwrap.ini");
if (latest is null)
{
Console.Error.WriteLine("[-] Failed to download latest INI from GitHub.");
return unchecked((int)NativeMethods.ERROR_ACCESS_DENIED);
}
if (!TryGetIniDate(null, latest, out int newDate))
return unchecked((int)NativeMethods.ERROR_ACCESS_DENIED);
Console.WriteLine($"[*] Latest update date: {FormatDate(newDate)}");
if (newDate == oldDate)
{
Console.WriteLine("[*] Everything is up to date.");
return 0;
}
if (newDate > oldDate)
{
Console.WriteLine("[+] New update is available, updating...");
CheckTermsrvProcess();
Console.WriteLine("[*] Terminating service...");
SecurityHelper.AddPrivilege(NativeMethods.SE_DEBUG_NAME);
ProcessHelper.KillProcess(_termServicePid);
Thread.Sleep(1000);
RestartSharedServices();
Thread.Sleep(500);
File.WriteAllText(iniPath, latest, System.Text.Encoding.UTF8);
Console.WriteLine($"[+] INI file updated: {iniPath}");
// Recompute version for offset generation
var fv = FileVersionHelper.GetVersionExpanded(_termServicePath);
if (fv is not null) _termSrvVerTxt = fv.ToString();
Console.WriteLine("[*] Checking INI coverage for installed termsrv.dll version...");
TryAutoGenerateOffsets();
ServiceHelper.StartService(TermService);
Console.WriteLine("[+] Update completed.");
}
else
{
Console.WriteLine("[*] Your INI file is newer than public file. Are you a developer? :)");
}
return 0;
}
// ── Helpers ───────────────────────────────────────────────────────────────
private void RestartSharedServices()
{
foreach (var svc in _shareServices)
ServiceHelper.StartService(svc);
}
/// <summary>
/// Parses the <c>Updated=YYYYMMDD</c> line from an INI file or string.
/// Mirrors the Delphi CheckINIDate function.
/// </summary>
private static bool TryGetIniDate(string? filePath, string? content, out int date)
{
date = 0;
IEnumerable<string> lines;
if (filePath is not null)
{
if (!File.Exists(filePath))
{
Console.Error.WriteLine("[-] Failed to read INI file.");
return false;
}
lines = File.ReadLines(filePath);
}
else
{
lines = (content ?? string.Empty).Split('\n');
}
foreach (var line in lines)
{
var trimmed = line.TrimEnd('\r');
if (!trimmed.StartsWith("Updated=", StringComparison.Ordinal)) continue;
var raw = trimmed["Updated=".Length..].Replace("-", "");
if (int.TryParse(raw, out date)) return true;
Console.Error.WriteLine("[-] Wrong INI date format.");
return false;
}
Console.Error.WriteLine("[-] Failed to check INI date (Updated= line not found).");
return false;
}
private static string FormatDate(int d)
{
int y = d / 10000, m = (d / 100) % 100, day = d % 100;
return $"{y}.{m:D2}.{day:D2}";
}
}

@ -0,0 +1,114 @@
// Copyright 2026 sjackson0109 — Apache License 2.0
//
// RDPWInst — RDP Wrapper Library Installer
using RDPWrap.Common;
namespace RDPWInst;
internal static class Program
{
private static string Banner
{
get
{
var v = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
string version = v is null ? "unknown" : $"{v.Major}.{v.Minor}.{v.Build}";
return
$"RDP Wrapper Library\r\n" +
$"Installer v{version} (C# edition)\r\n" +
"Copyright (C) Stas'M Corp. 2018\r\n" +
"Maintained by sjackson0109 2026\r\n";
}
}
private const string Usage =
"USAGE:\r\n" +
"RDPWInst.exe [-l|-i[-s][-o][-f]|-w|-u[-k]|-r]\r\n\r\n" +
"-l display the license agreement\r\n" +
"-i install wrapper to Program Files folder (default)\r\n" +
"-i -s install wrapper to System32 folder\r\n" +
"-i -o online install mode (loads latest INI file)\r\n" +
"-i -f force install: silently uninstall existing installation first\r\n" +
"-w get latest update for INI file\r\n" +
"-u uninstall wrapper\r\n" +
"-u -k uninstall wrapper and keep settings\r\n" +
"-r force restart Terminal Services\r\n";
internal static int Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine(Banner);
// Validate args
if (args.Length < 1 ||
(args[0] != "-l" &&
args[0] != "-i" &&
args[0] != "-w" &&
args[0] != "-u" &&
args[0] != "-r"))
{
Console.WriteLine(Usage);
Pause();
return 0;
}
// -l print license
if (args[0] == "-l")
{
var license = ResourceHelper.ReadText("RDPWInst.Resources.license.txt",
System.Reflection.Assembly.GetExecutingAssembly());
Console.WriteLine(license ?? "(license resource not found)");
Pause();
return 0;
}
// Windows Vista / Server 2008 minimum check
if (Environment.OSVersion.Version < new Version(6, 0))
{
Console.Error.WriteLine("[-] Unsupported Windows version:");
Console.Error.WriteLine(" only >= 6.0 (Vista, Server 2008 and newer) are supported.");
Pause();
return 1;
}
if (!ArchHelper.IsSupported)
{
Console.Error.WriteLine("[-] Unsupported processor architecture.");
Pause();
return 1;
}
var engine = new InstallerEngine();
engine.CheckInstall();
int rc = args[0] switch
{
"-i" => engine.Install(
toSystem32: args.Contains("-s"),
online: args.Contains("-o"),
force: args.Contains("-f")),
"-u" => engine.Uninstall(keepSettings: args.Contains("-k")),
"-w" => engine.Update(),
"-r" => engine.Restart(),
_ => 0
};
Pause();
return rc;
}
/// <summary>
/// Waits for a keypress only when the process owns its console window
/// (i.e. was launched via double-click or UAC elevation rather than
/// piped/redirected from a script).
/// </summary>
private static void Pause()
{
if (!Console.IsInputRedirected && !Console.IsOutputRedirected)
{
Console.WriteLine("\nPress any key to exit...");
Console.ReadKey(intercept: true);
}
}
}

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>RDPWInst</RootNamespace>
<AssemblyName>RDPWInst</AssemblyName>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Description>Command-line installer and manager for RDP Wrapper — installs rdpwrap.dll and configures the Terminal Services layer.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RDPWrap\RDPWrap.csproj" />
</ItemGroup>
<!--
Embedded payload resources.
Place the compiled DLL binaries and rdpwrap.ini into src-csharp/RDPWInst/Resources/
before building. The resource names referenced in InstallerEngine.cs are:
RDPWInst.Resources.rdpw32.dll
RDPWInst.Resources.rdpw64.dll
RDPWInst.Resources.rdpwrap.ini
RDPWInst.Resources.rdpclip6032.exe
RDPWInst.Resources.rdpclip6064.exe
RDPWInst.Resources.rdpclip6132.exe
RDPWInst.Resources.rdpclip6164.exe
RDPWInst.Resources.rfxvmt32.dll
RDPWInst.Resources.rfxvmt64.dll
RDPWInst.Resources.license.txt
-->
<ItemGroup>
<EmbeddedResource Include="Resources\rdpw32.dll" Condition="Exists('Resources\rdpw32.dll')" />
<EmbeddedResource Include="Resources\rdpw64.dll" Condition="Exists('Resources\rdpw64.dll')" />
<EmbeddedResource Include="Resources\rdpwrap.ini" Condition="Exists('Resources\rdpwrap.ini')" />
<EmbeddedResource Include="Resources\rdpclip6032.exe" Condition="Exists('Resources\rdpclip6032.exe')" />
<EmbeddedResource Include="Resources\rdpclip6064.exe" Condition="Exists('Resources\rdpclip6064.exe')" />
<EmbeddedResource Include="Resources\rdpclip6132.exe" Condition="Exists('Resources\rdpclip6132.exe')" />
<EmbeddedResource Include="Resources\rdpclip6164.exe" Condition="Exists('Resources\rdpclip6164.exe')" />
<EmbeddedResource Include="Resources\rfxvmt32.dll" Condition="Exists('Resources\rfxvmt32.dll')" />
<EmbeddedResource Include="Resources\rfxvmt64.dll" Condition="Exists('Resources\rfxvmt64.dll')" />
<EmbeddedResource Include="Resources\license.txt" Condition="Exists('Resources\license.txt')" />
</ItemGroup>
</Project>

@ -0,0 +1,31 @@
# RDPWInst/Resources
Place the compiled binary payloads here before building RDPWInst.exe.
These files are embedded as manifest resources at build time via `<EmbeddedResource Condition="Exists(...)">` entries in `RDPWInst.csproj` — if a file is absent the resource is simply omitted and the build still succeeds (online install mode is used as fallback).
## CI-staged files (automatically copied by `build-and-release.yml`)
| File | Source | Used when |
|---------------|--------------------------------------------------|-----------------------------|
| `rdpw32.dll` | Build output of `src-x86-x64-Fusix/` (Win32) | Always — 32-bit install |
| `rdpw64.dll` | Build output of `src-x86-x64-Fusix/` (x64) | Always — 64-bit install |
| `rdpwrap.ini` | `msi/rdpwrap.ini` from repo | Always — offline fallback |
| `license.txt` | Repo `LICENSE` (copied as plain text) | `RDPWInst -l` flag |
## Optional legacy files (not in VCS, not staged by CI)
| File | Purpose | Status / action required |
|-------------------|--------------------------------------------|--------------------------------------------------------------------------------------|
| `rdpclip6032.exe` | Updated rdpclip for Vista x86 | Not redistributable by this project — obtain from original stascorp release if needed |
| `rdpclip6064.exe` | Updated rdpclip for Vista x64 | Same as above |
| `rdpclip6132.exe` | Updated rdpclip for Win7 x86 | Same as above |
| `rdpclip6164.exe` | Updated rdpclip for Win7 x64 | Same as above |
| `rfxvmt32.dll` | RemoteFX codec for Win10 Home x86 | **Not redistributable** — must be extracted from a Windows 10 Home installation at `C:\Windows\System32\rfxvmt.dll`. See [#194](https://github.com/stascorp/rdpwrap/issues/194) for context. |
| `rfxvmt64.dll` | RemoteFX codec for Win10 Home x64 | Same as above |
> **Decision note:** `rfxvmt.dll` is a Microsoft-owned component and cannot be legally bundled.
> `InstallerEngine.cs` handles the missing-rfxvmt case at runtime: if the file is absent from
> the embedded resources and absent from the install directory, a warning is printed and the
> user is directed to copy it manually. No CI step attempts to source or stage these files.
All binary files in this folder are excluded from version control via `.gitignore`.

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="RDPWInst.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- requireAdministrator: installer must run elevated. -->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 / Server 2016+ -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
<!-- Windows 8.1 / Server 2012 R2 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 8 / Server 2012 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 7 / Server 2008 R2 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows Vista / Server 2008 -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
</application>
</compatibility>
</assembly>

@ -0,0 +1,10 @@
{
"version": 1,
"dependencies": {
"net10.0-windows7.0": {
"rdpwrap": {
"type": "Project"
}
}
}
}

@ -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", "RDPWrap\RDPWrap.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

@ -0,0 +1,80 @@
// Copyright 2026 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 2026 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 2026 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 2026 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 2026 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 2026 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,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<RootNamespace>RDPWrap.Common</RootNamespace>
<AssemblyName>RDPWrap</AssemblyName>
<Description>Shared helper library (P/Invoke, registry, service, security helpers) for the RDP Wrapper tool suite.</Description>
</PropertyGroup>
<!-- Grant all three consumer assemblies access to internal members (e.g. NativeMethods) -->
<ItemGroup>
<InternalsVisibleTo Include="RDPWInst" />
<InternalsVisibleTo Include="RDPConf" />
<InternalsVisibleTo Include="RDPCheck" />
</ItemGroup>
</Project>

@ -0,0 +1,97 @@
// Copyright 2026 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 2026 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 2026 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 2026 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,6 @@
{
"version": 1,
"dependencies": {
"net10.0-windows7.0": {}
}
}

File diff suppressed because it is too large Load Diff

@ -1,106 +0,0 @@
 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{AF9BEAA3-99CD-4B2E-BE67-3F3BD27B961A}</ProjectGuid>
<ProjectVersion>12.0</ProjectVersion>
<MainSource>RDPWInst.dpr</MainSource>
<Config Condition="'$(Config)'==''">Release</Config>
<DCC_DCCCompiler>DCC32</DCC_DCCCompiler>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Release' or '$(Cfg_1)'!=''">
<Cfg_1>true</Cfg_1>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Debug' or '$(Cfg_2)'!=''">
<Cfg_2>true</Cfg_2>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Base)'!=''">
<DCC_ExeOutput>..\bin\</DCC_ExeOutput>
<DCC_UnitAlias>WinTypes=Windows;WinProcs=Windows;DbiTypes=BDE;DbiProcs=BDE;DbiErrs=BDE;$(DCC_UnitAlias)</DCC_UnitAlias>
<DCC_DependencyCheckOutputName>..\bin\RDPWInst.exe</DCC_DependencyCheckOutputName>
<DCC_ImageBase>00400000</DCC_ImageBase>
<DCC_Platform>x86</DCC_Platform>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
<DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>
<DCC_SymbolReferenceInfo>0</DCC_SymbolReferenceInfo>
<DCC_DebugInformation>false</DCC_DebugInformation>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>
</PropertyGroup>
<ItemGroup>
<DelphiCompile Include="RDPWInst.dpr">
<MainSource>MainSource</MainSource>
</DelphiCompile>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
<BuildConfiguration Include="Release">
<Key>Cfg_1</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
<BuildConfiguration Include="Debug">
<Key>Cfg_2</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
</ItemGroup>
<Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')"/>
<ProjectExtensions>
<Borland.Personality>Delphi.Personality.12</Borland.Personality>
<Borland.ProjectType/>
<BorlandProject>
<Delphi.Personality>
<Parameters>
<Parameters Name="UseLauncher">False</Parameters>
<Parameters Name="LoadAllSymbols">True</Parameters>
<Parameters Name="LoadUnspecifiedSymbols">False</Parameters>
<Parameters Name="RunParams">-w</Parameters>
</Parameters>
<VersionInfo>
<VersionInfo Name="IncludeVerInfo">False</VersionInfo>
<VersionInfo Name="AutoIncBuild">False</VersionInfo>
<VersionInfo Name="MajorVer">1</VersionInfo>
<VersionInfo Name="MinorVer">0</VersionInfo>
<VersionInfo Name="Release">0</VersionInfo>
<VersionInfo Name="Build">0</VersionInfo>
<VersionInfo Name="Debug">False</VersionInfo>
<VersionInfo Name="PreRelease">False</VersionInfo>
<VersionInfo Name="Special">False</VersionInfo>
<VersionInfo Name="Private">False</VersionInfo>
<VersionInfo Name="DLL">False</VersionInfo>
<VersionInfo Name="Locale">1049</VersionInfo>
<VersionInfo Name="CodePage">1251</VersionInfo>
</VersionInfo>
<VersionInfoKeys>
<VersionInfoKeys Name="CompanyName"/>
<VersionInfoKeys Name="FileDescription"/>
<VersionInfoKeys Name="FileVersion">1.0.0.0</VersionInfoKeys>
<VersionInfoKeys Name="InternalName"/>
<VersionInfoKeys Name="LegalCopyright"/>
<VersionInfoKeys Name="LegalTrademarks"/>
<VersionInfoKeys Name="OriginalFilename"/>
<VersionInfoKeys Name="ProductName"/>
<VersionInfoKeys Name="ProductVersion">1.0.0.0</VersionInfoKeys>
<VersionInfoKeys Name="Comments"/>
</VersionInfoKeys>
<Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\bcboffice2k140.bpl">Embarcadero C++Builder Office 2000 Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\bcbofficexp140.bpl">Embarcadero C++Builder Office XP Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\dcloffice2k140.bpl">Microsoft Office 2000 Sample Automation Server Wrapper Components</Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\dclofficexp140.bpl">Microsoft Office XP Sample Automation Server Wrapper Components</Excluded_Packages>
</Excluded_Packages>
<Source>
<Source Name="MainSource">RDPWInst.dpr</Source>
</Source>
</Delphi.Personality>
</BorlandProject>
<ProjectFileVersion>12</ProjectFileVersion>
</ProjectExtensions>
</Project>

Binary file not shown.

@ -1,29 +0,0 @@
object Frm: TFrm
Left = 0
Top = 0
BorderIcons = [biSystemMenu, biMinimize]
BorderStyle = bsSingle
Caption = 'Local RDP Checker'
ClientHeight = 480
ClientWidth = 640
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
Position = poDesktopCenter
OnCreate = FormCreate
PixelsPerInch = 96
TextHeight = 13
object RDP: TMsRdpClient2
Left = 0
Top = 0
Width = 640
Height = 480
TabOrder = 0
OnDisconnected = RDPDisconnected
ControlData = {0003000008000200000000000B0000000B000000}
end
end

@ -1,160 +0,0 @@
{
Copyright 2015 Stas'M Corp.
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.
}
unit MainUnit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, OleServer, MSTSCLib_TLB, OleCtrls, Registry;
type
TFrm = class(TForm)
RDP: TMsRdpClient2;
procedure RDPDisconnected(ASender: TObject; discReason: Integer);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Frm: TFrm;
SecurityLayer, UserAuthentication: DWORD;
implementation
{$R *.dfm}
procedure TFrm.FormCreate(Sender: TObject);
var
Reg: TRegistry;
begin
RDP.DisconnectedText := 'Disconnected.';
RDP.ConnectingText := 'Connecting...';
RDP.ConnectedStatusText := 'Connected.';
RDP.UserName := '';
RDP.Server := '127.0.0.2';
Reg := TRegistry.Create;
Reg.RootKey := HKEY_LOCAL_MACHINE;
if Reg.OpenKey('\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp', True) then
begin
try
SecurityLayer := Reg.ReadInteger('SecurityLayer');
UserAuthentication := Reg.ReadInteger('UserAuthentication');
Reg.WriteInteger('SecurityLayer', 0);
Reg.WriteInteger('UserAuthentication', 0);
except
end;
Reg.CloseKey;
end;
if Reg.OpenKeyReadOnly('\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp') then begin
try
RDP.AdvancedSettings2.RDPPort := Reg.ReadInteger('PortNumber');
except
end;
Reg.CloseKey;
end;
Reg.Free;
Sleep(1000);
RDP.Connect;
end;
procedure TFrm.RDPDisconnected(ASender: TObject; discReason: Integer);
var
ErrStr: String;
Reg: TRegistry;
begin
case discReason of
1: ErrStr := 'Local disconnection.';
2: ErrStr := 'Disconnected by user.';
3: ErrStr := 'Disconnected by server.';
$904: ErrStr := 'Socket closed.';
$C08: ErrStr := 'Decompress error.';
$108: ErrStr := 'Connection timed out.';
$C06: ErrStr := 'Decryption error.';
$104: ErrStr := 'DNS name lookup failure.';
$508: ErrStr := 'DNS lookup failed.';
$B06: ErrStr := 'Encryption error.';
$604: ErrStr := 'Windows Sockets gethostbyname() call failed.';
$208: ErrStr := 'Host not found error.';
$408: ErrStr := 'Internal error.';
$906: ErrStr := 'Internal security error.';
$A06: ErrStr := 'Internal security error.';
$506: ErrStr := 'The encryption method specified is not valid.';
$804: ErrStr := 'Bad IP address specified.';
$606: ErrStr := 'Server security data is not valid.';
$406: ErrStr := 'Security data is not valid.';
$308: ErrStr := 'The IP address specified is not valid.';
$808: ErrStr := 'License negotiation failed.';
$908: ErrStr := 'Licensing time-out.';
$106: ErrStr := 'Out of memory.';
$206: ErrStr := 'Out of memory.';
$306: ErrStr := 'Out of memory.';
$706: ErrStr := 'Failed to unpack server certificate.';
$204: ErrStr := 'Socket connection failed.';
$404: ErrStr := 'Windows Sockets recv() call failed.';
$704: ErrStr := 'Time-out occurred.';
$608: ErrStr := 'Internal timer error.';
$304: ErrStr := 'Windows Sockets send() call failed.';
$B07: ErrStr := 'The account is disabled.';
$E07: ErrStr := 'The account is expired.';
$D07: ErrStr := 'The account is locked out.';
$C07: ErrStr := 'The account is restricted.';
$1B07: ErrStr := 'The received certificate is expired.';
$1607: ErrStr := 'The policy does not support delegation of credentials to the target server.';
$2107: ErrStr := 'The server authentication policy does not allow connection requests using saved credentials. The user must enter new credentials.';
$807: ErrStr := 'Login failed.';
$1807: ErrStr := 'No authority could be contacted for authentication. The domain name of the authenticating party could be wrong, the domain could be unreachable, or there might have been a trust relationship failure.';
$A07: ErrStr := 'The specified user has no account.';
$F07: ErrStr := 'The password is expired.';
$1207: ErrStr := 'The user password must be changed before logging on for the first time.';
$1707: ErrStr := 'Delegation of credentials to the target server is not allowed unless mutual authentication has been achieved.';
$2207: ErrStr := 'The smart card is blocked.';
$1C07: ErrStr := 'An incorrect PIN was presented to the smart card.';
$B09: ErrStr := 'Network Level Authentication is required, run RDPCheck as administrator.';
$708: ErrStr := 'RDP is working, but the client doesn''t allow loopback connections. Try to connect to your PC from another device in the network.';
else ErrStr := 'Unknown code 0x'+IntToHex(discReason, 1);
end;
if (discReason > 2) then
MessageBox(Handle, PWideChar(ErrStr), 'Disconnected', mb_Ok or mb_IconError);
Reg := TRegistry.Create;
Reg.RootKey := HKEY_LOCAL_MACHINE;
if Reg.OpenKey('\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp', True) then
begin
try
Reg.WriteInteger('SecurityLayer', SecurityLayer);
Reg.WriteInteger('UserAuthentication', UserAuthentication);
except
end;
Reg.CloseKey;
end;
Reg.Free;
Halt(0);
end;
end.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

@ -1,31 +0,0 @@
{
Copyright 2014 Stas'M Corp.
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.
}
program RDPCheck;
uses
Forms,
MainUnit in 'MainUnit.pas' {Frm};
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.Title := 'Local RDP Checker';
Application.CreateForm(TFrm, Frm);
Application.Run;
end.

@ -1,108 +0,0 @@
 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{90AE83F6-26B8-45D4-92FE-CF4ACCDE9F68}</ProjectGuid>
<ProjectVersion>12.0</ProjectVersion>
<MainSource>RDPCheck.dpr</MainSource>
<Config Condition="'$(Config)'==''">Release</Config>
<DCC_DCCCompiler>DCC32</DCC_DCCCompiler>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Release' or '$(Cfg_1)'!=''">
<Cfg_1>true</Cfg_1>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Debug' or '$(Cfg_2)'!=''">
<Cfg_2>true</Cfg_2>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Base)'!=''">
<DCC_ExeOutput>..\bin\</DCC_ExeOutput>
<DCC_UnitAlias>WinTypes=Windows;WinProcs=Windows;DbiTypes=BDE;DbiProcs=BDE;DbiErrs=BDE;$(DCC_UnitAlias)</DCC_UnitAlias>
<DCC_DependencyCheckOutputName>..\bin\RDPCheck.exe</DCC_DependencyCheckOutputName>
<DCC_ImageBase>00400000</DCC_ImageBase>
<DCC_Platform>x86</DCC_Platform>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
<DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>
<DCC_SymbolReferenceInfo>0</DCC_SymbolReferenceInfo>
<DCC_DebugInformation>false</DCC_DebugInformation>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>
</PropertyGroup>
<ItemGroup>
<DelphiCompile Include="RDPCheck.dpr">
<MainSource>MainSource</MainSource>
</DelphiCompile>
<DCCReference Include="MainUnit.pas">
<Form>Frm</Form>
</DCCReference>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
<BuildConfiguration Include="Release">
<Key>Cfg_1</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
<BuildConfiguration Include="Debug">
<Key>Cfg_2</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
</ItemGroup>
<Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')"/>
<ProjectExtensions>
<Borland.Personality>Delphi.Personality.12</Borland.Personality>
<Borland.ProjectType/>
<BorlandProject>
<Delphi.Personality>
<Parameters>
<Parameters Name="UseLauncher">False</Parameters>
<Parameters Name="LoadAllSymbols">True</Parameters>
<Parameters Name="LoadUnspecifiedSymbols">False</Parameters>
</Parameters>
<VersionInfo>
<VersionInfo Name="IncludeVerInfo">True</VersionInfo>
<VersionInfo Name="AutoIncBuild">False</VersionInfo>
<VersionInfo Name="MajorVer">2</VersionInfo>
<VersionInfo Name="MinorVer">2</VersionInfo>
<VersionInfo Name="Release">0</VersionInfo>
<VersionInfo Name="Build">0</VersionInfo>
<VersionInfo Name="Debug">False</VersionInfo>
<VersionInfo Name="PreRelease">False</VersionInfo>
<VersionInfo Name="Special">False</VersionInfo>
<VersionInfo Name="Private">False</VersionInfo>
<VersionInfo Name="DLL">False</VersionInfo>
<VersionInfo Name="Locale">1033</VersionInfo>
<VersionInfo Name="CodePage">1252</VersionInfo>
</VersionInfo>
<VersionInfoKeys>
<VersionInfoKeys Name="CompanyName">Stas&apos;M Corp.</VersionInfoKeys>
<VersionInfoKeys Name="FileDescription">Local RDP Checker</VersionInfoKeys>
<VersionInfoKeys Name="FileVersion">2.2.0.0</VersionInfoKeys>
<VersionInfoKeys Name="InternalName">RDPCheck</VersionInfoKeys>
<VersionInfoKeys Name="LegalCopyright">Copyright © Stas&apos;M Corp. 2015</VersionInfoKeys>
<VersionInfoKeys Name="LegalTrademarks">Stas&apos;M Corp.</VersionInfoKeys>
<VersionInfoKeys Name="OriginalFilename">RDPCheck.exe</VersionInfoKeys>
<VersionInfoKeys Name="ProductName">RDP Host Support</VersionInfoKeys>
<VersionInfoKeys Name="ProductVersion">1.6.0.0</VersionInfoKeys>
<VersionInfoKeys Name="Comments">http://stascorp.com</VersionInfoKeys>
</VersionInfoKeys>
<Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\bcboffice2k140.bpl">Embarcadero C++Builder Office 2000 Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\bcbofficexp140.bpl">Embarcadero C++Builder Office XP Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\dcloffice2k140.bpl">Microsoft Office 2000 Sample Automation Server Wrapper Components</Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\dclofficexp140.bpl">Microsoft Office XP Sample Automation Server Wrapper Components</Excluded_Packages>
</Excluded_Packages>
<Source>
<Source Name="MainSource">RDPCheck.dpr</Source>
</Source>
</Delphi.Personality>
</BorlandProject>
<ProjectFileVersion>12</ProjectFileVersion>
</ProjectExtensions>
</Project>

Binary file not shown.

@ -1,47 +0,0 @@
object LicenseForm: TLicenseForm
Left = 0
Top = 0
BorderIcons = []
BorderStyle = bsDialog
Caption = 'License Agreement'
ClientHeight = 344
ClientWidth = 386
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
Position = poOwnerFormCenter
PixelsPerInch = 96
TextHeight = 13
object mText: TMemo
Left = 8
Top = 8
Width = 370
Height = 297
ReadOnly = True
ScrollBars = ssBoth
TabOrder = 0
WordWrap = False
end
object bAccept: TButton
Left = 115
Top = 311
Width = 75
Height = 25
Caption = '&Accept'
ModalResult = 1
TabOrder = 1
end
object bDecline: TButton
Left = 196
Top = 311
Width = 75
Height = 25
Caption = '&Decline'
ModalResult = 2
TabOrder = 2
end
end

@ -1,43 +0,0 @@
{
Copyright 2014 Stas'M Corp.
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.
}
unit LicenseUnit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TLicenseForm = class(TForm)
mText: TMemo;
bAccept: TButton;
bDecline: TButton;
private
{ Private declarations }
public
{ Public declarations }
end;
var
LicenseForm: TLicenseForm;
implementation
{$R *.dfm}
end.

@ -1,241 +0,0 @@
object MainForm: TMainForm
Left = 0
Top = 0
BorderStyle = bsDialog
Caption = 'RDP Wrapper Configuration'
ClientHeight = 314
ClientWidth = 404
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
Position = poDesktopCenter
OnCloseQuery = FormCloseQuery
OnCreate = FormCreate
OnDestroy = FormDestroy
PixelsPerInch = 96
TextHeight = 13
object bOK: TButton
Left = 40
Top = 281
Width = 75
Height = 25
Caption = 'OK'
ModalResult = 1
TabOrder = 4
OnClick = bOKClick
end
object bCancel: TButton
Left = 121
Top = 281
Width = 75
Height = 25
Caption = 'Cancel'
ModalResult = 2
TabOrder = 5
OnClick = bCancelClick
end
object bApply: TButton
Left = 202
Top = 281
Width = 75
Height = 25
Caption = 'Apply'
Enabled = False
TabOrder = 6
OnClick = bApplyClick
end
object rgNLA: TRadioGroup
Left = 202
Top = 89
Width = 194
Height = 73
Caption = 'Authentication Mode'
Items.Strings = (
'GUI Authentication Only'
'Default RDP Authentication'
'Network Level Authentication')
TabOrder = 2
OnClick = cbAllowTSConnectionsClick
end
object rgShadow: TRadioGroup
Left = 202
Top = 168
Width = 194
Height = 105
Caption = 'Session Shadowing Mode'
Items.Strings = (
'Disable Shadowing'
'Full access with user'#39's permission'
'Full access without permission'
'View only with user'#39's permission'
'View only without permission')
TabOrder = 3
OnClick = cbAllowTSConnectionsClick
end
object bLicense: TButton
Left = 283
Top = 281
Width = 87
Height = 25
Caption = 'View license...'
TabOrder = 7
OnClick = bLicenseClick
end
object gbDiag: TGroupBox
Left = 8
Top = 6
Width = 388
Height = 77
Caption = 'Diagnostics'
TabOrder = 0
object lListener: TLabel
Left = 11
Top = 55
Width = 70
Height = 13
Caption = 'Listener state:'
end
object lService: TLabel
Left = 11
Top = 36
Width = 67
Height = 13
Caption = 'Service state:'
end
object lsListener: TLabel
Left = 91
Top = 55
Width = 44
Height = 13
Caption = 'Unknown'
end
object lsService: TLabel
Left = 91
Top = 36
Width = 44
Height = 13
Caption = 'Unknown'
end
object lsTSVer: TLabel
Left = 226
Top = 36
Width = 44
Height = 13
Caption = 'Unknown'
end
object lsWrapper: TLabel
Left = 91
Top = 17
Width = 44
Height = 13
Caption = 'Unknown'
end
object lsWrapVer: TLabel
Left = 226
Top = 17
Width = 44
Height = 13
Caption = 'Unknown'
end
object lTSVer: TLabel
Left = 202
Top = 36
Width = 20
Height = 13
Caption = 'ver.'
end
object lWrapper: TLabel
Left = 11
Top = 17
Width = 74
Height = 13
Caption = 'Wrapper state:'
end
object lWrapVer: TLabel
Left = 202
Top = 17
Width = 20
Height = 13
Caption = 'ver.'
end
object lsSuppVer: TLabel
Left = 202
Top = 55
Width = 70
Height = 13
Caption = '[support level]'
end
end
object gbGeneral: TGroupBox
Left = 8
Top = 89
Width = 188
Height = 184
Caption = 'General Settings'
TabOrder = 1
object lRDPPort: TLabel
Left = 8
Top = 44
Width = 47
Height = 13
Caption = 'RDP port:'
end
object cbAllowTSConnections: TCheckBox
Left = 8
Top = 18
Width = 132
Height = 17
Caption = 'Enable Remote Desktop'
TabOrder = 0
OnClick = cbAllowTSConnectionsClick
end
object cbSingleSessionPerUser: TCheckBox
Left = 8
Top = 69
Width = 129
Height = 17
Caption = 'Single session per user'
TabOrder = 2
OnClick = cbAllowTSConnectionsClick
end
object cbHideUsers: TCheckBox
Left = 8
Top = 92
Width = 149
Height = 17
Caption = 'Hide users on logon screen'
TabOrder = 3
OnClick = cbAllowTSConnectionsClick
end
object seRDPPort: TSpinEdit
Left = 61
Top = 41
Width = 62
Height = 22
MaxValue = 65535
MinValue = 0
TabOrder = 1
Value = 0
OnChange = seRDPPortChange
end
object cbCustomPrg: TCheckBox
Left = 8
Top = 115
Width = 169
Height = 17
Caption = 'Allow to start custom programs'
TabOrder = 4
OnClick = cbAllowTSConnectionsClick
end
end
object Timer: TTimer
Interval = 250
OnTimer = TimerTimer
Left = 352
Top = 27
end
end

@ -1,722 +0,0 @@
{
Copyright 2017 Stas'M Corp.
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.
}
unit MainUnit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Spin, ExtCtrls, Registry, WinSvc;
type
TMainForm = class(TForm)
bOK: TButton;
bCancel: TButton;
bApply: TButton;
cbSingleSessionPerUser: TCheckBox;
rgNLA: TRadioGroup;
cbAllowTSConnections: TCheckBox;
rgShadow: TRadioGroup;
seRDPPort: TSpinEdit;
lRDPPort: TLabel;
lService: TLabel;
lListener: TLabel;
lWrapper: TLabel;
lsListener: TLabel;
lsService: TLabel;
lsWrapper: TLabel;
Timer: TTimer;
lTSVer: TLabel;
lsTSVer: TLabel;
lWrapVer: TLabel;
lsWrapVer: TLabel;
bLicense: TButton;
gbDiag: TGroupBox;
lsSuppVer: TLabel;
cbHideUsers: TCheckBox;
gbGeneral: TGroupBox;
cbCustomPrg: TCheckBox;
procedure FormCreate(Sender: TObject);
procedure cbAllowTSConnectionsClick(Sender: TObject);
procedure seRDPPortChange(Sender: TObject);
procedure bApplyClick(Sender: TObject);
procedure bCancelClick(Sender: TObject);
procedure bOKClick(Sender: TObject);
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
procedure bLicenseClick(Sender: TObject);
procedure TimerTimer(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
function ExecWait(Cmdline: String): Boolean;
procedure ReadSettings;
procedure WriteSettings;
end;
FILE_VERSION = record
Version: record case Boolean of
True: (dw: DWORD);
False: (w: record
Minor, Major: Word;
end;)
end;
Release, Build: Word;
bDebug, bPrerelease, bPrivate, bSpecial: Boolean;
end;
WTS_SESSION_INFOW = record
SessionId: DWORD;
Name: packed array [0..33] of WideChar;
State: DWORD;
end;
WTS_SESSION = Array[0..0] of WTS_SESSION_INFOW;
PWTS_SESSION_INFOW = ^WTS_SESSION;
const
winstadll = 'winsta.dll';
var
MainForm: TMainForm;
Ready: Boolean = False;
Arch: Byte;
OldWow64RedirectionValue: LongBool;
OldPort: Word;
INI: String;
function WinStationEnumerateW(hServer: THandle;
var ppSessionInfo: PWTS_SESSION_INFOW; var pCount: DWORD): BOOL; stdcall;
external winstadll name 'WinStationEnumerateW';
function WinStationFreeMemory(P: Pointer): BOOL; stdcall; external winstadll;
implementation
{$R *.dfm}
{$R resource.res}
uses
LicenseUnit;
function ExpandPath(Path: String): String;
var
Str: Array[0..511] of Char;
begin
Result := '';
FillChar(Str, 512, 0);
if Arch = 64 then
Path := StringReplace(Path, '%ProgramFiles%', '%ProgramW6432%', [rfReplaceAll, rfIgnoreCase]);
if ExpandEnvironmentStrings(PWideChar(Path), Str, 512) > 0 then
Result := Str;
end;
function DisableWowRedirection: Boolean;
type
TFunc = function(var Wow64FsEnableRedirection: LongBool): LongBool; stdcall;
var
hModule: THandle;
Wow64DisableWow64FsRedirection: TFunc;
begin
Result := False;
hModule := GetModuleHandle(kernel32);
if hModule <> 0 then
Wow64DisableWow64FsRedirection := GetProcAddress(hModule, 'Wow64DisableWow64FsRedirection')
else
Exit;
if @Wow64DisableWow64FsRedirection <> nil then
Result := Wow64DisableWow64FsRedirection(OldWow64RedirectionValue);
end;
function RevertWowRedirection: Boolean;
type
TFunc = function(var Wow64RevertWow64FsRedirection: LongBool): LongBool; stdcall;
var
hModule: THandle;
Wow64RevertWow64FsRedirection: TFunc;
begin
Result := False;
hModule := GetModuleHandle(kernel32);
if hModule <> 0 then
Wow64RevertWow64FsRedirection := GetProcAddress(hModule, 'Wow64RevertWow64FsRedirection')
else
Exit;
if @Wow64RevertWow64FsRedirection <> nil then
Result := Wow64RevertWow64FsRedirection(OldWow64RedirectionValue);
end;
function GetFileVersion(const FileName: TFileName; var FileVersion: FILE_VERSION): Boolean;
type
VS_VERSIONINFO = record
wLength, wValueLength, wType: Word;
szKey: Array[1..16] of WideChar;
Padding1: Word;
Value: VS_FIXEDFILEINFO;
Padding2, Children: Word;
end;
PVS_VERSIONINFO = ^VS_VERSIONINFO;
const
VFF_DEBUG = 1;
VFF_PRERELEASE = 2;
VFF_PRIVATE = 8;
VFF_SPECIAL = 32;
var
hFile: HMODULE;
hResourceInfo: HRSRC;
VersionInfo: PVS_VERSIONINFO;
begin
Result := False;
hFile := LoadLibraryEx(PWideChar(FileName), 0, LOAD_LIBRARY_AS_DATAFILE);
if hFile = 0 then
Exit;
hResourceInfo := FindResource(hFile, PWideChar(1), PWideChar($10));
if hResourceInfo = 0 then
Exit;
VersionInfo := Pointer(LoadResource(hFile, hResourceInfo));
if VersionInfo = nil then
Exit;
FileVersion.Version.dw := VersionInfo.Value.dwFileVersionMS;
FileVersion.Release := Word(VersionInfo.Value.dwFileVersionLS shr 16);
FileVersion.Build := Word(VersionInfo.Value.dwFileVersionLS);
FileVersion.bDebug := (VersionInfo.Value.dwFileFlags and VFF_DEBUG) = VFF_DEBUG;
FileVersion.bPrerelease := (VersionInfo.Value.dwFileFlags and VFF_PRERELEASE) = VFF_PRERELEASE;
FileVersion.bPrivate := (VersionInfo.Value.dwFileFlags and VFF_PRIVATE) = VFF_PRIVATE;
FileVersion.bSpecial := (VersionInfo.Value.dwFileFlags and VFF_SPECIAL) = VFF_SPECIAL;
FreeLibrary(hFile);
Result := True;
end;
function IsWrapperInstalled(var WrapperPath: String): ShortInt;
var
TermServiceHost,
TermServicePath: String;
Reg: TRegistry;
begin
Result := -1;
WrapperPath := '';
Reg := TRegistry.Create;
Reg.RootKey := HKEY_LOCAL_MACHINE;
if not Reg.OpenKeyReadOnly('\SYSTEM\CurrentControlSet\Services\TermService') then begin
Reg.Free;
Exit;
end;
TermServiceHost := Reg.ReadString('ImagePath');
Reg.CloseKey;
if Pos('svchost.exe', LowerCase(TermServiceHost)) = 0 then
begin
Result := 2;
Reg.Free;
Exit;
end;
if not Reg.OpenKeyReadOnly('\SYSTEM\CurrentControlSet\Services\TermService\Parameters') then
begin
Reg.Free;
Exit;
end;
TermServicePath := Reg.ReadString('ServiceDll');
Reg.CloseKey;
Reg.Free;
if (Pos('termsrv.dll', LowerCase(TermServicePath)) = 0)
and (Pos('rdpwrap.dll', LowerCase(TermServicePath)) = 0) then
begin
Result := 2;
Exit;
end;
if Pos('rdpwrap.dll', LowerCase(TermServicePath)) > 0 then begin
WrapperPath := TermServicePath;
Result := 1;
end else
Result := 0;
end;
function GetTermSrvState: ShortInt;
type
SERVICE_STATUS_PROCESS = record
dwServiceType,
dwCurrentState,
dwControlsAccepted,
dwWin32ExitCode,
dwServiceSpecificExitCode,
dwCheckPoint,
dwWaitHint,
dwProcessId,
dwServiceFlags: DWORD;
end;
PSERVICE_STATUS_PROCESS = ^SERVICE_STATUS_PROCESS;
const
SvcName = 'TermService';
var
hSC: SC_HANDLE;
hSvc: THandle;
lpServiceStatusProcess: PSERVICE_STATUS_PROCESS;
Buf: Pointer;
cbBufSize, pcbBytesNeeded: Cardinal;
begin
Result := -1;
hSC := OpenSCManager(nil, SERVICES_ACTIVE_DATABASE, SC_MANAGER_CONNECT);
if hSC = 0 then
Exit;
hSvc := OpenService(hSC, PWideChar(SvcName), SERVICE_QUERY_STATUS);
if hSvc = 0 then
begin
CloseServiceHandle(hSC);
Exit;
end;
if QueryServiceStatusEx(hSvc, SC_STATUS_PROCESS_INFO, nil, 0, pcbBytesNeeded) then
Exit;
cbBufSize := pcbBytesNeeded;
GetMem(Buf, cbBufSize);
if not QueryServiceStatusEx(hSvc, SC_STATUS_PROCESS_INFO, Buf, cbBufSize, pcbBytesNeeded) then begin
FreeMem(Buf, cbBufSize);
CloseServiceHandle(hSvc);
CloseServiceHandle(hSC);
Exit;
end else begin
lpServiceStatusProcess := Buf;
Result := ShortInt(lpServiceStatusProcess^.dwCurrentState);
end;
FreeMem(Buf, cbBufSize);
CloseServiceHandle(hSvc);
CloseServiceHandle(hSC);
end;
function IsListenerWorking: Boolean;
var
pCount: DWORD;
SessionInfo: PWTS_SESSION_INFOW;
I: Integer;
begin
Result := False;
if not WinStationEnumerateW(0, SessionInfo, pCount) then
Exit;
for I := 0 to pCount - 1 do
if SessionInfo^[I].Name = 'RDP-Tcp' then begin
Result := True;
Break;
end;
WinStationFreeMemory(SessionInfo);
end;
function ExtractResText(ResName: String): String;
var
ResStream: TResourceStream;
Str: TStringList;
begin
ResStream := TResourceStream.Create(HInstance, ResName, RT_RCDATA);
Str := TStringList.Create;
try
Str.LoadFromStream(ResStream);
except
end;
ResStream.Free;
Result := Str.Text;
Str.Free;
end;
function TMainForm.ExecWait(Cmdline: String): Boolean;
var
si: STARTUPINFO;
pi: PROCESS_INFORMATION;
begin
Result := False;
ZeroMemory(@si, sizeof(si));
si.cb := sizeof(si);
si.dwFlags := STARTF_USESHOWWINDOW;
si.wShowWindow := SW_HIDE;
UniqueString(Cmdline);
if not CreateProcess(nil, PWideChar(Cmdline), nil, nil, True, 0, nil, nil, si, pi) then begin
MessageBox(Handle,
PWideChar('CreateProcess error (code: ' + IntToStr(GetLastError) + ').'),
'Error', MB_ICONERROR or MB_OK);
Exit;
end;
CloseHandle(pi.hThread);
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
Result := True;
end;
procedure TMainForm.ReadSettings;
var
Reg: TRegistry;
SecurityLayer, UserAuthentication: Integer;
begin
Reg := TRegistry.Create;
Reg.RootKey := HKEY_LOCAL_MACHINE;
Reg.OpenKeyReadOnly('\SYSTEM\CurrentControlSet\Control\Terminal Server');
try
cbAllowTSConnections.Checked := not Reg.ReadBool('fDenyTSConnections');
except
end;
try
cbSingleSessionPerUser.Checked := Reg.ReadBool('fSingleSessionPerUser');
except
end;
try
cbCustomPrg.Checked := Reg.ReadBool('HonorLegacySettings');
except
end;
Reg.CloseKey;
Reg.OpenKeyReadOnly('\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp');
seRDPPort.Value := 3389;
try
seRDPPort.Value := Reg.ReadInteger('PortNumber');
except
end;
OldPort := seRDPPort.Value;
SecurityLayer := 0;
UserAuthentication := 0;
try
SecurityLayer := Reg.ReadInteger('SecurityLayer');
UserAuthentication := Reg.ReadInteger('UserAuthentication');
except
end;
if (SecurityLayer = 0) and (UserAuthentication = 0) then
rgNLA.ItemIndex := 0;
if (SecurityLayer = 1) and (UserAuthentication = 0) then
rgNLA.ItemIndex := 1;
if (SecurityLayer = 2) and (UserAuthentication = 1) then
rgNLA.ItemIndex := 2;
try
rgShadow.ItemIndex := Reg.ReadInteger('Shadow');
except
end;
Reg.CloseKey;
Reg.OpenKeyReadOnly('\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System');
try
cbHideUsers.Checked := Reg.ReadBool('dontdisplaylastusername');
except
end;
Reg.CloseKey;
Reg.Free;
end;
procedure TMainForm.WriteSettings;
var
Reg: TRegistry;
SecurityLayer, UserAuthentication: Integer;
begin
Reg := TRegistry.Create;
Reg.RootKey := HKEY_LOCAL_MACHINE;
Reg.OpenKey('\SYSTEM\CurrentControlSet\Control\Terminal Server', True);
try
Reg.WriteBool('fDenyTSConnections', not cbAllowTSConnections.Checked);
except
end;
try
Reg.WriteBool('fSingleSessionPerUser', cbSingleSessionPerUser.Checked);
except
end;
try
Reg.WriteBool('HonorLegacySettings', cbCustomPrg.Checked);
except
end;
Reg.CloseKey;
Reg.OpenKey('\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp', True);
try
Reg.WriteInteger('PortNumber', seRDPPort.Value);
except
end;
if OldPort <> seRDPPort.Value then
begin
OldPort := seRDPPort.Value;
ExecWait('netsh advfirewall firewall set rule name="Remote Desktop" new localport=' + IntToStr(OldPort));
end;
case rgNLA.ItemIndex of
0: begin
SecurityLayer := 0;
UserAuthentication := 0;
end;
1: begin
SecurityLayer := 1;
UserAuthentication := 0;
end;
2: begin
SecurityLayer := 2;
UserAuthentication := 1;
end;
else begin
SecurityLayer := -1;
UserAuthentication := -1;
end;
end;
if SecurityLayer >= 0 then begin
try
Reg.WriteInteger('SecurityLayer', SecurityLayer);
Reg.WriteInteger('UserAuthentication', UserAuthentication);
except
end;
end;
if rgShadow.ItemIndex >= 0 then begin
try
Reg.WriteInteger('Shadow', rgShadow.ItemIndex);
except
end;
end;
Reg.CloseKey;
Reg.OpenKey('\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services', True);
if rgShadow.ItemIndex >= 0 then begin
try
Reg.WriteInteger('Shadow', rgShadow.ItemIndex);
except
end;
end;
Reg.CloseKey;
Reg.OpenKey('\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System', True);
try
Reg.WriteBool('dontdisplaylastusername', cbHideUsers.Checked);
except
end;
Reg.CloseKey;
Reg.Free;
end;
function CheckSupport(FV: FILE_VERSION): Byte;
var
VerTxt: String;
begin
Result := 0;
if (FV.Version.w.Major = 6) and (FV.Version.w.Minor = 0) then
Result := 1;
if (FV.Version.w.Major = 6) and (FV.Version.w.Minor = 1) then
Result := 1;
VerTxt := Format('%d.%d.%d.%d',
[FV.Version.w.Major, FV.Version.w.Minor, FV.Release, FV.Build]);
if Pos('[' + VerTxt + ']', INI) > 0 then
Result := 2;
end;
procedure TMainForm.TimerTimer(Sender: TObject);
var
WrapperPath, INIPath: String;
FV: FILE_VERSION;
L: TStringList;
CheckSupp: Boolean;
begin
CheckSupp := False;
case IsWrapperInstalled(WrapperPath) of
-1: begin
lsWrapper.Caption := 'Unknown';
lsWrapper.Font.Color := clGrayText;
end;
0: begin
lsWrapper.Caption := 'Not installed';
lsWrapper.Font.Color := clGrayText;
end;
1: begin
lsWrapper.Caption := 'Installed';
lsWrapper.Font.Color := clGreen;
CheckSupp := True;
INIPath := ExtractFilePath(ExpandPath(WrapperPath)) + 'rdpwrap.ini';
if not FileExists(INIPath) then
CheckSupp := False;
end;
2: begin
lsWrapper.Caption := '3rd-party';
lsWrapper.Font.Color := clRed;
end;
end;
case GetTermSrvState of
-1, 0: begin
lsService.Caption := 'Unknown';
lsService.Font.Color := clGrayText;
end;
SERVICE_STOPPED: begin
lsService.Caption := 'Stopped';
lsService.Font.Color := clRed;
end;
SERVICE_START_PENDING: begin
lsService.Caption := 'Starting...';
lsService.Font.Color := clGrayText;
end;
SERVICE_STOP_PENDING: begin
lsService.Caption := 'Stopping...';
lsService.Font.Color := clGrayText;
end;
SERVICE_RUNNING: begin
lsService.Caption := 'Running';
lsService.Font.Color := clGreen;
end;
SERVICE_CONTINUE_PENDING: begin
lsService.Caption := 'Resuming...';
lsService.Font.Color := clGrayText;
end;
SERVICE_PAUSE_PENDING: begin
lsService.Caption := 'Suspending...';
lsService.Font.Color := clGrayText;
end;
SERVICE_PAUSED: begin
lsService.Caption := 'Suspended';
lsService.Font.Color := clWindowText;
end;
end;
if IsListenerWorking then begin
lsListener.Caption := 'Listening';
lsListener.Font.Color := clGreen;
end else begin
lsListener.Caption := 'Not listening';
lsListener.Font.Color := clRed;
end;
if WrapperPath = '' then begin
lsWrapVer.Caption := 'N/A';
lsWrapVer.Font.Color := clGrayText;
end else
if not GetFileVersion(ExpandPath(WrapperPath), FV) then begin
lsWrapVer.Caption := 'N/A';
lsWrapVer.Font.Color := clGrayText;
end else begin
lsWrapVer.Caption :=
IntToStr(FV.Version.w.Major)+'.'+
IntToStr(FV.Version.w.Minor)+'.'+
IntToStr(FV.Release)+'.'+
IntToStr(FV.Build);
lsWrapVer.Font.Color := clWindowText;
end;
if not GetFileVersion('termsrv.dll', FV) then begin
lsTSVer.Caption := 'N/A';
lsTSVer.Font.Color := clGrayText;
end else begin
lsTSVer.Caption :=
IntToStr(FV.Version.w.Major)+'.'+
IntToStr(FV.Version.w.Minor)+'.'+
IntToStr(FV.Release)+'.'+
IntToStr(FV.Build);
lsTSVer.Font.Color := clWindowText;
lsSuppVer.Visible := CheckSupp;
if CheckSupp then begin
if INI = '' then begin
L := TStringList.Create;
try
L.LoadFromFile(INIPath);
except
end;
INI := L.Text;
L.Free;
end;
case CheckSupport(FV) of
0: begin
lsSuppVer.Caption := '[not supported]';
lsSuppVer.Font.Color := clRed;
end;
1: begin
lsSuppVer.Caption := '[supported partially]';
lsSuppVer.Font.Color := clOlive;
end;
2: begin
lsSuppVer.Caption := '[fully supported]';
lsSuppVer.Font.Color := clGreen;
end;
end;
end;
end;
end;
procedure TMainForm.bLicenseClick(Sender: TObject);
begin
LicenseForm.mText.Text := ExtractResText('LICENSE');
if LicenseForm.ShowModal <> mrOk then
Halt(0);
end;
procedure TMainForm.cbAllowTSConnectionsClick(Sender: TObject);
begin
if Ready then
bApply.Enabled := True;
end;
procedure TMainForm.seRDPPortChange(Sender: TObject);
begin
if Ready then
bApply.Enabled := True;
end;
procedure TMainForm.FormCreate(Sender: TObject);
var
SI: TSystemInfo;
begin
GetNativeSystemInfo(SI);
case SI.wProcessorArchitecture of
0: Arch := 32;
6: Arch := 64; // Itanium-based x64
9: Arch := 64; // Intel/AMD x64
else Arch := 0;
end;
if Arch = 64 then
DisableWowRedirection;
ReadSettings;
Ready := True;
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
if Arch = 64 then
RevertWowRedirection;
end;
procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
if bApply.Enabled then
CanClose := MessageBox(Handle, 'Settings are not saved. Do you want to exit?',
'Warning', mb_IconWarning or mb_YesNo) = mrYes;
end;
procedure TMainForm.bOKClick(Sender: TObject);
begin
if bApply.Enabled then begin
WriteSettings;
bApply.Enabled := False;
end;
Close;
end;
procedure TMainForm.bCancelClick(Sender: TObject);
begin
Close;
end;
procedure TMainForm.bApplyClick(Sender: TObject);
begin
WriteSettings;
bApply.Enabled := False;
end;
end.

@ -1,33 +0,0 @@
{
Copyright 2014 Stas'M Corp.
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.
}
program RDPConf;
uses
Forms,
MainUnit in 'MainUnit.pas' {MainForm},
LicenseUnit in 'LicenseUnit.pas' {LicenseForm};
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.Title := 'Remote Desktop Protocol Configuration';
Application.CreateForm(TMainForm, MainForm);
Application.CreateForm(TLicenseForm, LicenseForm);
Application.Run;
end.

@ -1,111 +0,0 @@
 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{A7CB4C30-85F5-4D96-B510-6F0CDCF7C2DA}</ProjectGuid>
<ProjectVersion>12.0</ProjectVersion>
<MainSource>RDPConf.dpr</MainSource>
<Config Condition="'$(Config)'==''">Debug</Config>
<DCC_DCCCompiler>DCC32</DCC_DCCCompiler>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Release' or '$(Cfg_1)'!=''">
<Cfg_1>true</Cfg_1>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Debug' or '$(Cfg_2)'!=''">
<Cfg_2>true</Cfg_2>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Base)'!=''">
<DCC_ExeOutput>..\bin\</DCC_ExeOutput>
<DCC_UnitAlias>WinTypes=Windows;WinProcs=Windows;DbiTypes=BDE;DbiProcs=BDE;DbiErrs=BDE;$(DCC_UnitAlias)</DCC_UnitAlias>
<DCC_DependencyCheckOutputName>..\bin\RDPConf.exe</DCC_DependencyCheckOutputName>
<DCC_ImageBase>00400000</DCC_ImageBase>
<DCC_Platform>x86</DCC_Platform>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
<DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>
<DCC_SymbolReferenceInfo>0</DCC_SymbolReferenceInfo>
<DCC_DebugInformation>false</DCC_DebugInformation>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>
</PropertyGroup>
<ItemGroup>
<DelphiCompile Include="RDPConf.dpr">
<MainSource>MainSource</MainSource>
</DelphiCompile>
<DCCReference Include="MainUnit.pas">
<Form>MainForm</Form>
</DCCReference>
<DCCReference Include="LicenseUnit.pas">
<Form>LicenseForm</Form>
</DCCReference>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
<BuildConfiguration Include="Release">
<Key>Cfg_1</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
<BuildConfiguration Include="Debug">
<Key>Cfg_2</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
</ItemGroup>
<Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')"/>
<ProjectExtensions>
<Borland.Personality>Delphi.Personality.12</Borland.Personality>
<Borland.ProjectType/>
<BorlandProject>
<Delphi.Personality>
<Source>
<Source Name="MainSource">RDPConf.dpr</Source>
</Source>
<Parameters>
<Parameters Name="UseLauncher">False</Parameters>
<Parameters Name="LoadAllSymbols">True</Parameters>
<Parameters Name="LoadUnspecifiedSymbols">False</Parameters>
</Parameters>
<VersionInfo>
<VersionInfo Name="IncludeVerInfo">False</VersionInfo>
<VersionInfo Name="AutoIncBuild">False</VersionInfo>
<VersionInfo Name="MajorVer">1</VersionInfo>
<VersionInfo Name="MinorVer">0</VersionInfo>
<VersionInfo Name="Release">0</VersionInfo>
<VersionInfo Name="Build">0</VersionInfo>
<VersionInfo Name="Debug">False</VersionInfo>
<VersionInfo Name="PreRelease">False</VersionInfo>
<VersionInfo Name="Special">False</VersionInfo>
<VersionInfo Name="Private">False</VersionInfo>
<VersionInfo Name="DLL">False</VersionInfo>
<VersionInfo Name="Locale">1033</VersionInfo>
<VersionInfo Name="CodePage">1252</VersionInfo>
</VersionInfo>
<VersionInfoKeys>
<VersionInfoKeys Name="CompanyName">Stas&apos;M Corp.</VersionInfoKeys>
<VersionInfoKeys Name="FileDescription">RDP Configuration Program</VersionInfoKeys>
<VersionInfoKeys Name="FileVersion">1.0.0.0</VersionInfoKeys>
<VersionInfoKeys Name="InternalName">RDPConf</VersionInfoKeys>
<VersionInfoKeys Name="LegalCopyright">Copyright © Stas&apos;M Corp. 2014</VersionInfoKeys>
<VersionInfoKeys Name="LegalTrademarks">Stas&apos;M Corp.</VersionInfoKeys>
<VersionInfoKeys Name="OriginalFilename">RDPConf.exe</VersionInfoKeys>
<VersionInfoKeys Name="ProductName">RDP Host Support</VersionInfoKeys>
<VersionInfoKeys Name="ProductVersion">1.4.0.0</VersionInfoKeys>
<VersionInfoKeys Name="Comments">http://stascorp.com</VersionInfoKeys>
</VersionInfoKeys>
<Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\bcboffice2k140.bpl">Embarcadero C++Builder Office 2000 Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\bcbofficexp140.bpl">Embarcadero C++Builder Office XP Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\dcloffice2k140.bpl">Microsoft Office 2000 Sample Automation Server Wrapper Components</Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\dclofficexp140.bpl">Microsoft Office XP Sample Automation Server Wrapper Components</Excluded_Packages>
</Excluded_Packages>
</Delphi.Personality>
</BorlandProject>
<ProjectFileVersion>12</ProjectFileVersion>
</ProjectExtensions>
</Project>

Binary file not shown.

Binary file not shown.

@ -1,375 +0,0 @@
{
Copyright 2014 Stas'M Corp.
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.
}
unit LiteINI;
interface
uses
SysUtils;
type
SList = Array of String;
INIValue = record
Name: String;
Value: String;
end;
INISection = record
Name: String;
Values: Array of INIValue;
end;
INIFile = Array of INISection;
procedure SListClear(var List: SList);
function SListAppend(var List: SList; S: String): Integer;
function SListFind(List: SList; Value: String): Integer;
function INIFindSection(INI: INIFile; Section: String): Integer;
function INIFindValue(INI: INIFile; Section: Integer; Value: String): Integer;
function INIAddSection(var INI: INIFile; Section: String): Integer;
function INIAddValue(var INI: INIFile; Section: Integer; ValueName, Value: String): Integer;
procedure INIUnload(var INI: INIFile);
procedure INILoad(var INI: INIFile; FileName: String);
function INISectionExists(INI: INIFile; Section: String): Boolean;
function INIValueExists(INI: INIFile; Section: String; Value: String): Boolean;
function INIReadSectionLowAPI(INI: INIFile; Section: Integer; var List: SList): Boolean;
function INIReadSection(INI: INIFile; Section: String): SList;
function INIReadStringLowAPI(INI: INIFile; Section, Value: Integer; var Str: String): Boolean;
function INIReadString(INI: INIFile; Section, Value, Default: String): String;
function INIReadInt(INI: INIFile; Section, Value: String; Default: Integer): Integer;
function INIReadDWord(INI: INIFile; Section, Value: String; Default: Cardinal): Cardinal;
function INIReadIntHex(INI: INIFile; Section, Value: String; Default: Integer): Integer;
function INIReadDWordHex(INI: INIFile; Section, Value: String; Default: Cardinal): Cardinal;
function INIReadBool(INI: INIFile; Section, Value: String; Default: Boolean): Boolean;
function INIReadBytes(INI: INIFile; Section, Value: String): TBytes;
function INIReadBytesDef(INI: INIFile; Section, Value: String; Default: TBytes): TBytes;
implementation
procedure SListClear(var List: SList);
begin
SetLength(List, 0);
end;
function SListAppend(var List: SList; S: String): Integer;
begin
SetLength(List, Length(List) + 1);
List[Length(List) - 1] := S;
Result := Length(List) - 1;
end;
function SListFind(List: SList; Value: String): Integer;
var
I: Integer;
begin
Result := -1;
for I := 0 to Length(List) - 1 do
if List[I] = Value then begin
Result := I;
Break;
end;
end;
function INIFindSection(INI: INIFile; Section: String): Integer;
var
I: Integer;
begin
Result := -1;
for I := 0 to Length(INI) - 1 do
if INI[I].Name = Section then begin
Result := I;
Exit;
end;
end;
function INIFindValue(INI: INIFile; Section: Integer; Value: String): Integer;
var
I: Integer;
begin
Result := -1;
if (Section < 0) or (Section >= Length(INI)) then
Exit;
for I := 0 to Length(INI[Section].Values) - 1 do
if INI[Section].Values[I].Name = Value then begin
Result := I;
Exit;
end;
end;
function INIAddSection(var INI: INIFile; Section: String): Integer;
begin
Result := INIFindSection(INI, Section);
if Result >= 0 then
Exit;
Result := Length(INI);
SetLength(INI, Result + 1);
INI[Result].Name := Section;
SetLength(INI[Result].Values, 0);
end;
function INIAddValue(var INI: INIFile; Section: Integer; ValueName, Value: String): Integer;
var
I: Integer;
begin
Result := -1;
if (Section < 0) or (Section >= Length(INI)) then
Exit;
I := INIFindValue(INI, Section, ValueName);
if I = -1 then begin
Result := Length(INI[Section].Values);
SetLength(INI[Section].Values, Result + 1);
INI[Section].Values[Result].Name := ValueName;
INI[Section].Values[Result].Value := Value;
end else begin
INI[Section].Values[I].Value := Value;
Result := I;
end;
end;
procedure INIUnload(var INI: INIFile);
begin
SetLength(INI, 0);
end;
procedure INILoad(var INI: INIFile; FileName: String);
var
F: TextFile;
S, ValueName, Value: String;
INIList: SList;
I, Sect: Integer;
begin
INIUnload(INI);
if not FileExists(FileName) then
Exit;
AssignFile(F, FileName);
Reset(F);
// Read and filter lines
while not EOF(F) do begin
Readln(F, S);
if (Pos(';', S) <> 1)
and (Pos('#', S) <> 1)
and (
((Pos('[', S) > 0) and (Pos(']', S) > 0)) or
(Pos('=', S) > 0)
)
then
SListAppend(INIList, S);
end;
CloseFile(F);
// Parse 2 (parse format)
Sect := -1;
for I := 0 to Length(INIList) - 1 do begin
S := Trim(INIList[I]);
if Length(S) >= 2 then
if (S[1] = '[') and (S[Length(S)] = ']') then begin
S := Trim(Copy(S, 2, Length(S) - 2));
Sect := INIAddSection(INI, S);
Continue;
end;
S := INIList[I];
if Pos('=', S) > 0 then begin
ValueName := Trim(Copy(S, 1, Pos('=', S) - 1));
Value := Copy(S, Pos('=', S) + 1, Length(S) - Pos('=', S));
if Sect = -1 then
Sect := INIAddSection(INI, '');
INIAddValue(INI, Sect, ValueName, Value);
end;
end;
end;
function INISectionExists(INI: INIFile; Section: String): Boolean;
begin
Result := INIFindSection(INI, Section) > -1;
end;
function INIValueExists(INI: INIFile; Section: String; Value: String): Boolean;
var
Sect: Integer;
begin
Sect := INIFindSection(INI, Section);
Result := INIFindValue(INI, Sect, Value) > -1;
end;
function INIReadSectionLowAPI(INI: INIFile; Section: Integer; var List: SList): Boolean;
var
I: Integer;
begin
Result := False;
SetLength(List, 0);
if (Section < 0) or (Section >= Length(INI)) then
Exit;
for I := 0 to Length(INI[Section].Values) - 1 do
SListAppend(List, INI[Section].Values[I].Name);
Result := True;
end;
function INIReadSection(INI: INIFile; Section: String): SList;
var
Sect: Integer;
begin
Sect := INIFindSection(INI, Section);
INIReadSectionLowAPI(INI, Sect, Result);
end;
function INIReadStringLowAPI(INI: INIFile; Section, Value: Integer; var Str: String): Boolean;
begin
Result := False;
if (Section < 0) or (Section >= Length(INI)) then
Exit;
if (Value < 0) or (Value >= Length(INI[Section].Values)) then
Exit;
Str := INI[Section].Values[Value].Value;
Result := True;
end;
function INIReadString(INI: INIFile; Section, Value, Default: String): String;
var
Sect, Val: Integer;
begin
Sect := INIFindSection(INI, Section);
Val := INIFindValue(INI, Sect, Value);
if not INIReadStringLowAPI(INI, Sect, Val, Result) then
Result := Default;
end;
function INIReadInt(INI: INIFile; Section, Value: String; Default: Integer): Integer;
var
S: String;
E: Integer;
begin
S := INIReadString(INI, Section, Value, '');
Val(S, Result, E);
if E <> 0 then
Result := Default;
end;
function INIReadDWord(INI: INIFile; Section, Value: String; Default: Cardinal): Cardinal;
var
S: String;
E: Integer;
begin
S := INIReadString(INI, Section, Value, '');
Val(S, Result, E);
if E <> 0 then
Result := Default;
end;
function INIReadIntHex(INI: INIFile; Section, Value: String; Default: Integer): Integer;
var
S: String;
E: Integer;
begin
S := INIReadString(INI, Section, Value, '');
Val('$'+S, Result, E);
if E <> 0 then
Result := Default;
end;
function INIReadDWordHex(INI: INIFile; Section, Value: String; Default: Cardinal): Cardinal;
var
S: String;
E: Integer;
begin
S := INIReadString(INI, Section, Value, '');
Val('$'+S, Result, E);
if E <> 0 then
Result := Default;
end;
function INIReadBool(INI: INIFile; Section, Value: String; Default: Boolean): Boolean;
var
S: String;
I: Cardinal;
E: Integer;
begin
S := INIReadString(INI, Section, Value, '');
Val(S, I, E);
if E <> 0 then
Result := Default
else
Result := I > 0;
end;
function StringToBytes(S: String; var B: TBytes): Boolean;
var
I: Integer;
begin
Result := False;
if Odd(Length(S)) then
Exit;
SetLength(B, Length(S) div 2);
for I := 0 to Length(B) - 1 do begin
B[I] := 0;
case S[(I*2)+2] of
'0': ;
'1': B[I] := B[I] or $1;
'2': B[I] := B[I] or $2;
'3': B[I] := B[I] or $3;
'4': B[I] := B[I] or $4;
'5': B[I] := B[I] or $5;
'6': B[I] := B[I] or $6;
'7': B[I] := B[I] or $7;
'8': B[I] := B[I] or $8;
'9': B[I] := B[I] or $9;
'A','a': B[I] := B[I] or $A;
'B','b': B[I] := B[I] or $B;
'C','c': B[I] := B[I] or $C;
'D','d': B[I] := B[I] or $D;
'E','e': B[I] := B[I] or $E;
'F','f': B[I] := B[I] or $F;
else Exit;
end;
case S[(I*2)+1] of
'0': ;
'1': B[I] := B[I] or $10;
'2': B[I] := B[I] or $20;
'3': B[I] := B[I] or $30;
'4': B[I] := B[I] or $40;
'5': B[I] := B[I] or $50;
'6': B[I] := B[I] or $60;
'7': B[I] := B[I] or $70;
'8': B[I] := B[I] or $80;
'9': B[I] := B[I] or $90;
'A','a': B[I] := B[I] or $A0;
'B','b': B[I] := B[I] or $B0;
'C','c': B[I] := B[I] or $C0;
'D','d': B[I] := B[I] or $D0;
'E','e': B[I] := B[I] or $E0;
'F','f': B[I] := B[I] or $F0;
else Exit;
end;
end;
Result := True;
end;
function INIReadBytes(INI: INIFile; Section, Value: String): TBytes;
var
S: String;
begin
S := INIReadString(INI, Section, Value, '');
if not StringToBytes(S, Result) then
SetLength(Result, 0);
end;
function INIReadBytesDef(INI: INIFile; Section, Value: String; Default: TBytes): TBytes;
var
S: String;
begin
S := INIReadString(INI, Section, Value, '');
if not StringToBytes(S, Result) then
Result := Default;
end;
end.

@ -1,737 +0,0 @@
{
Copyright 2014 Stas'M Corp.
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.
}
library rdpwrap;
uses
SysUtils,
Windows,
TlHelp32,
LiteINI;
{$R rdpwrap.res}
// Hook core definitions
type
OldCode = packed record
One: DWORD;
two: Word;
end;
far_jmp = packed record
PushOp: Byte;
PushArg: Pointer;
RetOp: Byte;
end;
mov_far_jmp = packed record
MovOp: Byte;
MovArg: Byte;
PushOp: Byte;
PushArg: Pointer;
RetOp: Byte;
end;
TTHREADENTRY32 = packed record
dwSize: DWORD;
cntUsage: DWORD;
th32ThreadID: DWORD;
th32OwnerProcessID: DWORD;
tpBasePri: LongInt;
tpDeltaPri: LongInt;
dwFlags: DWORD;
end;
//IntArray = Array of Integer;
FILE_VERSION = record
Version: record case Boolean of
True: (dw: DWORD);
False: (w: record
Minor, Major: Word;
end;)
end;
Release, Build: Word;
bDebug, bPrerelease, bPrivate, bSpecial: Boolean;
end;
const
THREAD_SUSPEND_RESUME = 2;
TH32CS_SNAPTHREAD = 4;
var
INI: INIFile;
LogFile: String = '\rdpwrap.txt';
bw: {$if CompilerVersion>=16} NativeUInt {$else} DWORD {$endif};
IsHooked: Boolean = False;
// Unhooked import
function OpenThread(dwDesiredAccess: DWORD; bInheritHandle: BOOL;
dwThreadId: DWORD): DWORD; stdcall; external kernel32;
function CreateToolhelp32Snapshot(dwFlags, th32ProcessID: DWORD): DWORD;
stdcall; external kernel32;
function Thread32First(hSnapshot: THandle; var lpte: TTHREADENTRY32): bool;
stdcall; external kernel32;
function Thread32Next(hSnapshot: THandle; var lpte: TTHREADENTRY32): bool;
stdcall; external kernel32;
// Wrapped import
var
TSMain: function(dwArgc: DWORD; lpszArgv: PWideChar): DWORD; stdcall;
TSGlobals: function(lpGlobalData: Pointer): DWORD; stdcall;
// Hooked import and vars
var
SLGetWindowsInformationDWORD: function(pwszValueName: PWideChar;
pdwValue: PDWORD): HRESULT; stdcall;
TermSrvBase: Pointer;
FV: FILE_VERSION;
var
Stub_SLGetWindowsInformationDWORD: far_jmp;
Old_SLGetWindowsInformationDWORD: OldCode;
// Main code
procedure WriteLog(S: AnsiString);
var
F: TextFile;
begin
if not FileExists(LogFile) then
Exit;
AssignFile(F, LogFile);
Append(F);
Write(F, S+#13#10);
CloseFile(F);
end;
function GetModuleHandleEx(dwFlags: DWORD; lpModuleName: PWideChar;
var phModule: HMODULE): BOOL; stdcall; external kernel32 name 'GetModuleHandleExW';
function GetCurrentModule: HMODULE;
const
GET_MODULE_HANDLE_EX_FLAG_PIN = 1;
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 2;
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS = 4;
begin
Result := 0;
GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, @GetCurrentModule, Result);
end;
function GetBinaryPath: String;
var
Buf: Array[0..511] of Byte;
begin
ZeroMemory(@Buf[0], Length(Buf));
GetModuleFileName(GetCurrentModule, PWideChar(@Buf[0]), Length(Buf));
Result := PWideChar(@Buf[0]);
end;
procedure StopThreads;
var
h, CurrTh, ThrHandle, CurrPr: DWORD;
Thread: TTHREADENTRY32;
begin
CurrTh := GetCurrentThreadId;
CurrPr := GetCurrentProcessId;
h := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if h <> INVALID_HANDLE_VALUE then
begin
Thread.dwSize := SizeOf(TTHREADENTRY32);
if Thread32First(h, Thread) then
repeat
if (Thread.th32ThreadID <> CurrTh) and
(Thread.th32OwnerProcessID = CurrPr) then
begin
ThrHandle := OpenThread(THREAD_SUSPEND_RESUME, false,
Thread.th32ThreadID);
if ThrHandle > 0 then
begin
SuspendThread(ThrHandle);
CloseHandle(ThrHandle);
end;
end;
until not Thread32Next(h, Thread);
CloseHandle(h);
end;
end;
procedure RunThreads;
var
h, CurrTh, ThrHandle, CurrPr: DWORD;
Thread: TTHREADENTRY32;
begin
CurrTh := GetCurrentThreadId;
CurrPr := GetCurrentProcessId;
h := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if h <> INVALID_HANDLE_VALUE then
begin
Thread.dwSize := SizeOf(TTHREADENTRY32);
if Thread32First(h, Thread) then
repeat
if (Thread.th32ThreadID <> CurrTh) and
(Thread.th32OwnerProcessID = CurrPr) then
begin
ThrHandle := OpenThread(THREAD_SUSPEND_RESUME, false,
Thread.th32ThreadID);
if ThrHandle > 0 then
begin
ResumeThread(ThrHandle);
CloseHandle(ThrHandle);
end;
end;
until not Thread32Next(h, Thread);
CloseHandle(h);
end;
end;
function GetModuleAddress(ModuleName: String; ProcessId: DWORD; var BaseAddr: Pointer; var BaseSize: DWORD): Boolean;
var
hSnap: THandle;
md: MODULEENTRY32;
begin
Result := False;
hSnap := CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, ProcessId);
if hSnap = INVALID_HANDLE_VALUE Then
Exit;
md.dwSize := SizeOf(MODULEENTRY32);
if Module32First(hSnap, md) then
begin
if LowerCase(ExtractFileName(md.szExePath)) = LowerCase(ModuleName) then
begin
Result := True;
BaseAddr := Pointer(md.modBaseAddr);
BaseSize := md.modBaseSize;
CloseHandle(hSnap);
Exit;
end;
while Module32Next(hSnap, md) Do
begin
if LowerCase(ExtractFileName(md.szExePath)) = LowerCase(ModuleName) then
begin
Result := True;
BaseAddr := Pointer(md.modBaseAddr);
BaseSize := md.modBaseSize;
Break;
end;
end;
end;
CloseHandle(hSnap);
end;
{procedure FindMem(Mem: Pointer; MemSz: DWORD; Buf: Pointer; BufSz: DWORD;
From: DWORD; var A: IntArray);
var
I: Integer;
begin
SetLength(A, 0);
I:=From;
if From>0 then
Inc(PByte(Mem), From);
while I < MemSz - BufSz + 1 do
begin
if (not IsBadReadPtr(Mem, BufSz)) and (CompareMem(Mem, Buf, BufSz)) then
begin
SetLength(A, Length(A)+1);
A[Length(A)-1] := I;
end;
Inc(I);
Inc(PByte(Mem));
end;
end;}
function GetModuleVersion(const ModuleName: String; var FileVersion: FILE_VERSION): Boolean;
type
VS_VERSIONINFO = record
wLength, wValueLength, wType: Word;
szKey: Array[1..16] of WideChar;
Padding1: Word;
Value: VS_FIXEDFILEINFO;
Padding2, Children: Word;
end;
PVS_VERSIONINFO = ^VS_VERSIONINFO;
const
VFF_DEBUG = 1;
VFF_PRERELEASE = 2;
VFF_PRIVATE = 8;
VFF_SPECIAL = 32;
var
hMod: HMODULE;
hResourceInfo: HRSRC;
VersionInfo: PVS_VERSIONINFO;
begin
Result := False;
if ModuleName = '' then
hMod := GetModuleHandle(nil)
else
hMod := GetModuleHandle(PWideChar(ModuleName));
if hMod = 0 then
Exit;
hResourceInfo := FindResource(hMod, PWideChar(1), PWideChar($10));
if hResourceInfo = 0 then
Exit;
VersionInfo := Pointer(LoadResource(hMod, hResourceInfo));
if VersionInfo = nil then
Exit;
FileVersion.Version.dw := VersionInfo.Value.dwFileVersionMS;
FileVersion.Release := Word(VersionInfo.Value.dwFileVersionLS shr 16);
FileVersion.Build := Word(VersionInfo.Value.dwFileVersionLS);
FileVersion.bDebug := (VersionInfo.Value.dwFileFlags and VFF_DEBUG) = VFF_DEBUG;
FileVersion.bPrerelease := (VersionInfo.Value.dwFileFlags and VFF_PRERELEASE) = VFF_PRERELEASE;
FileVersion.bPrivate := (VersionInfo.Value.dwFileFlags and VFF_PRIVATE) = VFF_PRIVATE;
FileVersion.bSpecial := (VersionInfo.Value.dwFileFlags and VFF_SPECIAL) = VFF_SPECIAL;
Result := True;
end;
function GetFileVersion(const FileName: String; var FileVersion: FILE_VERSION): Boolean;
type
VS_VERSIONINFO = record
wLength, wValueLength, wType: Word;
szKey: Array[1..16] of WideChar;
Padding1: Word;
Value: VS_FIXEDFILEINFO;
Padding2, Children: Word;
end;
PVS_VERSIONINFO = ^VS_VERSIONINFO;
const
VFF_DEBUG = 1;
VFF_PRERELEASE = 2;
VFF_PRIVATE = 8;
VFF_SPECIAL = 32;
var
hFile: HMODULE;
hResourceInfo: HRSRC;
VersionInfo: PVS_VERSIONINFO;
begin
Result := False;
hFile := LoadLibraryEx(PWideChar(FileName), 0, LOAD_LIBRARY_AS_DATAFILE);
if hFile = 0 then
Exit;
hResourceInfo := FindResource(hFile, PWideChar(1), PWideChar($10));
if hResourceInfo = 0 then
Exit;
VersionInfo := Pointer(LoadResource(hFile, hResourceInfo));
if VersionInfo = nil then
Exit;
FileVersion.Version.dw := VersionInfo.Value.dwFileVersionMS;
FileVersion.Release := Word(VersionInfo.Value.dwFileVersionLS shr 16);
FileVersion.Build := Word(VersionInfo.Value.dwFileVersionLS);
FileVersion.bDebug := (VersionInfo.Value.dwFileFlags and VFF_DEBUG) = VFF_DEBUG;
FileVersion.bPrerelease := (VersionInfo.Value.dwFileFlags and VFF_PRERELEASE) = VFF_PRERELEASE;
FileVersion.bPrivate := (VersionInfo.Value.dwFileFlags and VFF_PRIVATE) = VFF_PRIVATE;
FileVersion.bSpecial := (VersionInfo.Value.dwFileFlags and VFF_SPECIAL) = VFF_SPECIAL;
Result := True;
end;
function OverrideSL(ValueName: String; var Value: DWORD): Boolean;
begin
Result := True;
if INIValueExists(INI, 'SLPolicy', ValueName) then begin
Value := INIReadDWord(INI, 'SLPolicy', ValueName, 0);
Exit;
end;
Result := False;
end;
function New_SLGetWindowsInformationDWORD(pwszValueName: PWideChar;
pdwValue: PDWORD): HRESULT; stdcall;
var
dw: DWORD;
begin
// wrapped SLGetWindowsInformationDWORD function
// termsrv.dll will call this function instead of original SLC.dll
// Override SL Policy
WriteLog('Policy query: ' + pwszValueName);
if OverrideSL(pwszValueName, dw) then begin
pdwValue^ := dw;
Result := S_OK;
WriteLog('Policy rewrite: ' + IntToStr(pdwValue^));
Exit;
end;
// If the requested value name is not defined above
// revert to original SL Policy function
WriteProcessMemory(GetCurrentProcess, @SLGetWindowsInformationDWORD,
@Old_SLGetWindowsInformationDWORD, SizeOf(OldCode), bw);
// get result
Result := SLGetWindowsInformationDWORD(pwszValueName, pdwValue);
if Result = S_OK then
WriteLog('Policy result: ' + IntToStr(pdwValue^))
else
WriteLog('Policy request failed');
// wrap it back
WriteProcessMemory(GetCurrentProcess, @SLGetWindowsInformationDWORD,
@Stub_SLGetWindowsInformationDWORD, SizeOf(far_jmp), bw);
end;
function New_Win8SL(pwszValueName: PWideChar; pdwValue: PDWORD): HRESULT; register;
var
dw: DWORD;
begin
// wrapped unexported function SLGetWindowsInformationDWORDWrapper in termsrv.dll
// for Windows 8 support
// Override SL Policy
WriteLog('Policy query: ' + pwszValueName);
if OverrideSL(pwszValueName, dw) then begin
pdwValue^ := dw;
Result := S_OK;
WriteLog('Policy rewrite: ' + IntToStr(pdwValue^));
Exit;
end;
// If the requested value name is not defined above
// use function from SLC.dll
Result := SLGetWindowsInformationDWORD(pwszValueName, pdwValue);
if Result = S_OK then
WriteLog('Policy result: ' + IntToStr(pdwValue^))
else
WriteLog('Policy request failed');
end;
function New_Win8SL_CP(eax: DWORD; pdwValue: PDWORD; ecx: DWORD; pwszValueName: PWideChar): HRESULT; register;
begin
// wrapped unexported function SLGetWindowsInformationDWORDWrapper in termsrv.dll
// for Windows 8 Consumer Preview support
Result := New_Win8SL(pwszValueName, pdwValue);
end;
function New_CSLQuery_Initialize: HRESULT; stdcall;
var
Sect: String;
bServerSku,
bRemoteConnAllowed,
bFUSEnabled,
bAppServerAllowed,
bMultimonAllowed,
lMaxUserSessions,
ulMaxDebugSessions,
bInitialized: PDWORD;
begin
bServerSku := nil;
bRemoteConnAllowed := nil;
bFUSEnabled := nil;
bAppServerAllowed := nil;
bMultimonAllowed := nil;
lMaxUserSessions := nil;
ulMaxDebugSessions := nil;
bInitialized := nil;
WriteLog('>>> CSLQuery::Initialize');
Sect := IntToStr(FV.Version.w.Major)+'.'+IntToStr(FV.Version.w.Minor)+'.'+
IntToStr(FV.Release)+'.'+IntToStr(FV.Build)+'-SLInit';
if INISectionExists(INI, Sect) then begin
bServerSku := Pointer(Cardinal(TermSrvBase) + INIReadDWordHex(INI, Sect, 'bServerSku.x86', 0));
bRemoteConnAllowed := Pointer(Cardinal(TermSrvBase) + INIReadDWordHex(INI, Sect, 'bRemoteConnAllowed.x86', 0));
bFUSEnabled := Pointer(Cardinal(TermSrvBase) + INIReadDWordHex(INI, Sect, 'bFUSEnabled.x86', 0));
bAppServerAllowed := Pointer(Cardinal(TermSrvBase) + INIReadDWordHex(INI, Sect, 'bAppServerAllowed.x86', 0));
bMultimonAllowed := Pointer(Cardinal(TermSrvBase) + INIReadDWordHex(INI, Sect, 'bMultimonAllowed.x86', 0));
lMaxUserSessions := Pointer(Cardinal(TermSrvBase) + INIReadDWordHex(INI, Sect, 'lMaxUserSessions.x86', 0));
ulMaxDebugSessions := Pointer(Cardinal(TermSrvBase) + INIReadDWordHex(INI, Sect, 'ulMaxDebugSessions.x86', 0));
bInitialized := Pointer(Cardinal(TermSrvBase) + INIReadDWordHex(INI, Sect, 'bInitialized.x86', 0));
end;
if bServerSku <> nil then begin
bServerSku^ := INIReadDWord(INI, 'SLInit', 'bServerSku', 1);
WriteLog('SLInit [0x'+IntToHex(DWORD(bServerSku), 1)+'] bServerSku = ' + IntToStr(bServerSku^));
end;
if bRemoteConnAllowed <> nil then begin
bRemoteConnAllowed^ := INIReadDWord(INI, 'SLInit', 'bRemoteConnAllowed', 1);
WriteLog('SLInit [0x'+IntToHex(DWORD(bRemoteConnAllowed), 1)+'] bRemoteConnAllowed = ' + IntToStr(bRemoteConnAllowed^));
end;
if bFUSEnabled <> nil then begin
bFUSEnabled^ := INIReadDWord(INI, 'SLInit', 'bFUSEnabled', 1);
WriteLog('SLInit [0x'+IntToHex(DWORD(bFUSEnabled), 1)+'] bFUSEnabled = ' + IntToStr(bFUSEnabled^));
end;
if bAppServerAllowed <> nil then begin
bAppServerAllowed^ := INIReadDWord(INI, 'SLInit', 'bAppServerAllowed', 1);
WriteLog('SLInit [0x'+IntToHex(DWORD(bAppServerAllowed), 1)+'] bAppServerAllowed = ' + IntToStr(bAppServerAllowed^));
end;
if bMultimonAllowed <> nil then begin
bMultimonAllowed^ := INIReadDWord(INI, 'SLInit', 'bMultimonAllowed', 1);
WriteLog('SLInit [0x'+IntToHex(DWORD(bMultimonAllowed), 1)+'] bMultimonAllowed = ' + IntToStr(bMultimonAllowed^));
end;
if lMaxUserSessions <> nil then begin
lMaxUserSessions^ := INIReadDWord(INI, 'SLInit', 'lMaxUserSessions', 0);
WriteLog('SLInit [0x'+IntToHex(DWORD(lMaxUserSessions), 1)+'] lMaxUserSessions = ' + IntToStr(lMaxUserSessions^));
end;
if ulMaxDebugSessions <> nil then begin
ulMaxDebugSessions^ := INIReadDWord(INI, 'SLInit', 'ulMaxDebugSessions', 0);
WriteLog('SLInit [0x'+IntToHex(DWORD(ulMaxDebugSessions), 1)+'] ulMaxDebugSessions = ' + IntToStr(ulMaxDebugSessions^));
end;
if bInitialized <> nil then begin
bInitialized^ := INIReadDWord(INI, 'SLInit', 'bInitialized', 1);
WriteLog('SLInit [0x'+IntToHex(DWORD(bInitialized), 1)+'] bInitialized = ' + IntToStr(bInitialized^));
end;
Result := S_OK;
WriteLog('<<< CSLQuery::Initialize');
end;
procedure HookFunctions;
var
ConfigFile, Sect, FuncName: String;
V: DWORD;
TS_Handle, SLC_Handle: THandle;
TermSrvSize: DWORD;
SignPtr: Pointer;
I: Integer;
PatchList: SList;
Patch: Array of TBytes;
Jump: far_jmp;
MovJump: mov_far_jmp;
begin
{ hook function ^^
(called once) }
IsHooked := True;
TSMain := nil;
TSGlobals := nil;
SLGetWindowsInformationDWORD := nil;
WriteLog('Loading configuration...');
ConfigFile := ExtractFilePath(GetBinaryPath) + 'rdpwrap.ini';
WriteLog('Configuration file: ' + ConfigFile);
INILoad(INI, ConfigFile);
if Length(INI) = 0 then begin
WriteLog('Error: Failed to load configuration');
Exit;
end;
LogFile := INIReadString(INI, 'Main', 'LogFile', ExtractFilePath(GetBinaryPath) + 'rdpwrap.txt');
WriteLog('Initializing RDP Wrapper...');
// load termsrv.dll and get functions
TS_Handle := LoadLibrary('termsrv.dll');
if TS_Handle = 0 then begin
WriteLog('Error: Failed to load Terminal Services library');
Exit;
end;
TSMain := GetProcAddress(TS_Handle, 'ServiceMain');
TSGlobals := GetProcAddress(TS_Handle, 'SvchostPushServiceGlobals');
WriteLog(
'Base addr: 0x' + IntToHex(TS_Handle, 8) + #13#10 +
'SvcMain: termsrv.dll+0x' + IntToHex(Cardinal(@TSMain) - TS_Handle, 1) + #13#10 +
'SvcGlobals: termsrv.dll+0x' + IntToHex(Cardinal(@TSGlobals) - TS_Handle, 1)
);
V := 0;
// check termsrv version
if GetModuleVersion('termsrv.dll', FV) then
V := Byte(FV.Version.w.Minor) or (Byte(FV.Version.w.Major) shl 8)
else begin
// check NT version
// V := GetVersion; // deprecated
// V := ((V and $FF) shl 8) or ((V and $FF00) shr 8);
end;
if V = 0 then begin
WriteLog('Error: Failed to detect Terminal Services version');
Exit;
end;
WriteLog('Version: '+
IntToStr(FV.Version.w.Major)+'.'+
IntToStr(FV.Version.w.Minor)+'.'+
IntToStr(FV.Release)+'.'+
IntToStr(FV.Build));
// temporarily freeze threads
WriteLog('Freezing threads...');
StopThreads();
WriteLog('Caching patch codes...');
PatchList := INIReadSection(INI, 'PatchCodes');
SetLength(Patch, Length(PatchList));
for I := 0 to Length(Patch) - 1 do begin
Patch[I] := INIReadBytes(INI, 'PatchCodes', PatchList[I]);
if Length(Patch[I]) > 16 then // for security reasons
SetLength(Patch[I], 16); // not more than 16 bytes
end;
if (V = $0600) and (INIReadBool(INI, 'Main', 'SLPolicyHookNT60', True)) then begin
// Windows Vista
// uses SL Policy API (slc.dll)
// load slc.dll and hook function
SLC_Handle := LoadLibrary('slc.dll');
SLGetWindowsInformationDWORD := GetProcAddress(SLC_Handle, 'SLGetWindowsInformationDWORD');
if @SLGetWindowsInformationDWORD <> nil then
begin
// rewrite original function to call our function (make hook)
WriteLog('Hook SLGetWindowsInformationDWORD');
Stub_SLGetWindowsInformationDWORD.PushOp := $68;
Stub_SLGetWindowsInformationDWORD.PushArg := @New_SLGetWindowsInformationDWORD;
Stub_SLGetWindowsInformationDWORD.RetOp := $C3;
ReadProcessMemory(GetCurrentProcess, @SLGetWindowsInformationDWORD,
@Old_SLGetWindowsInformationDWORD, SizeOf(OldCode), bw);
WriteProcessMemory(GetCurrentProcess, @SLGetWindowsInformationDWORD,
@Stub_SLGetWindowsInformationDWORD, SizeOf(far_jmp), bw);
end;
end;
if (V = $0601) and (INIReadBool(INI, 'Main', 'SLPolicyHookNT61', True)) then begin
// Windows 7
// uses SL Policy API (slc.dll)
// load slc.dll and hook function
SLC_Handle := LoadLibrary('slc.dll');
SLGetWindowsInformationDWORD := GetProcAddress(SLC_Handle, 'SLGetWindowsInformationDWORD');
if @SLGetWindowsInformationDWORD <> nil then
begin
// rewrite original function to call our function (make hook)
WriteLog('Hook SLGetWindowsInformationDWORD');
Stub_SLGetWindowsInformationDWORD.PushOp := $68;
Stub_SLGetWindowsInformationDWORD.PushArg := @New_SLGetWindowsInformationDWORD;
Stub_SLGetWindowsInformationDWORD.RetOp := $C3;
ReadProcessMemory(GetCurrentProcess, @SLGetWindowsInformationDWORD,
@Old_SLGetWindowsInformationDWORD, SizeOf(OldCode), bw);
WriteProcessMemory(GetCurrentProcess, @SLGetWindowsInformationDWORD,
@Stub_SLGetWindowsInformationDWORD, SizeOf(far_jmp), bw);
end;
end;
if V = $0602 then begin
// Windows 8
// uses SL Policy internal unexported function
// load slc.dll and get function
// (will be used on intercepting undefined values)
SLC_Handle := LoadLibrary('slc.dll');
SLGetWindowsInformationDWORD := GetProcAddress(SLC_Handle, 'SLGetWindowsInformationDWORD');
end;
if V = $0603 then begin
// Windows 8.1
// uses SL Policy internal inline code
end;
if V = $0604 then begin
// Windows 10
// uses SL Policy internal inline code
end;
Sect := IntToStr(FV.Version.w.Major)+'.'+IntToStr(FV.Version.w.Minor)+'.'+
IntToStr(FV.Release)+'.'+IntToStr(FV.Build);
if INISectionExists(INI, Sect) then
if GetModuleAddress('termsrv.dll', GetCurrentProcessId, TermSrvBase, TermSrvSize) then begin
if INIReadBool(INI, Sect, 'LocalOnlyPatch.x86', False) then begin
WriteLog('Patch CEnforcementCore::GetInstanceOfTSLicense');
SignPtr := Pointer(Cardinal(TermSrvBase) + INIReadDWordHex(INI, Sect, 'LocalOnlyOffset.x86', 0));
I := SListFind(PatchList, INIReadString(INI, Sect, 'LocalOnlyCode.x86', ''));
if I >= 0 then
WriteProcessMemory(GetCurrentProcess, SignPtr, @Patch[I][0], Length(Patch[I]), bw);
end;
if INIReadBool(INI, Sect, 'SingleUserPatch.x86', False) then begin
WriteLog('Patch CSessionArbitrationHelper::IsSingleSessionPerUserEnabled');
SignPtr := Pointer(Cardinal(TermSrvBase) + INIReadDWordHex(INI, Sect, 'SingleUserOffset.x86', 0));
I := SListFind(PatchList, INIReadString(INI, Sect, 'SingleUserCode.x86', ''));
if I >= 0 then
WriteProcessMemory(GetCurrentProcess, SignPtr, @Patch[I][0], Length(Patch[I]), bw);
end;
if INIReadBool(INI, Sect, 'DefPolicyPatch.x86', False) then begin
WriteLog('Patch CDefPolicy::Query');
SignPtr := Pointer(Cardinal(TermSrvBase) + INIReadDWordHex(INI, Sect, 'DefPolicyOffset.x86', 0));
I := SListFind(PatchList, INIReadString(INI, Sect, 'DefPolicyCode.x86', ''));
if I >= 0 then
WriteProcessMemory(GetCurrentProcess, SignPtr, @Patch[I][0], Length(Patch[I]), bw);
end;
if INIReadBool(INI, Sect, 'SLPolicyInternal.x86', False) then begin
WriteLog('Hook SLGetWindowsInformationDWORDWrapper');
SignPtr := Pointer(Cardinal(TermSrvBase) + INIReadDWordHex(INI, Sect, 'SLPolicyOffset.x86', 0));
MovJump.MovOp := $89; // mov eax, ecx
MovJump.MovArg := $C8; // __msfastcall compatibility
MovJump.PushOp := $68;
MovJump.PushArg := @New_Win8SL;
MovJump.RetOp := $C3;
FuncName := INIReadString(INI, Sect, 'SLPolicyFunc.x86', 'New_Win8SL');
if FuncName = 'New_Win8SL' then
MovJump.PushArg := @New_Win8SL;
if FuncName = 'New_Win8SL_CP' then
MovJump.PushArg := @New_Win8SL_CP;
WriteProcessMemory(GetCurrentProcess, SignPtr,
@MovJump, SizeOf(mov_far_jmp), bw);
end;
if INIReadBool(INI, Sect, 'SLInitHook.x86', False) then begin
WriteLog('Hook CSLQuery::Initialize');
SignPtr := Pointer(Cardinal(TermSrvBase) + INIReadDWordHex(INI, Sect, 'SLInitOffset.x86', 0));
Jump.PushOp := $68;
Jump.PushArg := @New_CSLQuery_Initialize;
Jump.RetOp := $C3;
FuncName := INIReadString(INI, Sect, 'SLInitFunc.x86', 'New_CSLQuery_Initialize');
if FuncName = 'New_CSLQuery_Initialize' then
Jump.PushArg := @New_CSLQuery_Initialize;
WriteProcessMemory(GetCurrentProcess, SignPtr,
@Jump, SizeOf(far_jmp), bw);
end;
end;
// unfreeze threads
WriteLog('Resumimg threads...');
RunThreads();
end;
function TermServiceMain(dwArgc: DWORD; lpszArgv: PWideChar): DWORD; stdcall;
begin
// wrap ServiceMain function
WriteLog('>>> ServiceMain');
if not IsHooked then
HookFunctions;
Result := 0;
if @TSMain <> nil then
Result := TSMain(dwArgc, lpszArgv);
WriteLog('<<< ServiceMain');
end;
function TermServiceGlobals(lpGlobalData: Pointer): DWORD; stdcall;
begin
// wrap SvchostPushServiceGlobals function
WriteLog('>>> SvchostPushServiceGlobals');
if not IsHooked then
HookFunctions;
Result := 0;
if @TSGlobals <> nil then
Result := TSGlobals(lpGlobalData);
WriteLog('<<< SvchostPushServiceGlobals');
end;
// export section
exports
TermServiceMain index 1 name 'ServiceMain',
TermServiceGlobals index 2 name 'SvchostPushServiceGlobals';
begin
// DllMain procedure is not used
end.

@ -1,105 +0,0 @@
 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{D6811241-D595-4809-B3B8-13BECEA56E11}</ProjectGuid>
<MainSource>rdpwrap.dpr</MainSource>
<Config Condition="'$(Config)'==''">Release</Config>
<DCC_DCCCompiler>DCC32</DCC_DCCCompiler>
<ProjectVersion>12.0</ProjectVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Release' or '$(Cfg_1)'!=''">
<Cfg_1>true</Cfg_1>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Debug' or '$(Cfg_2)'!=''">
<Cfg_2>true</Cfg_2>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Base)'!=''">
<DCC_DependencyCheckOutputName>rdpwrap.dll</DCC_DependencyCheckOutputName>
<DCC_UnitAlias>WinTypes=Windows;WinProcs=Windows;$(DCC_UnitAlias)</DCC_UnitAlias>
<GenDll>true</GenDll>
<DCC_ImageBase>00400000</DCC_ImageBase>
<DCC_Platform>x86</DCC_Platform>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
<DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>
<DCC_SymbolReferenceInfo>0</DCC_SymbolReferenceInfo>
<DCC_DebugInformation>false</DCC_DebugInformation>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>
</PropertyGroup>
<ItemGroup>
<DelphiCompile Include="rdpwrap.dpr">
<MainSource>MainSource</MainSource>
</DelphiCompile>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
<BuildConfiguration Include="Release">
<Key>Cfg_1</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
<BuildConfiguration Include="Debug">
<Key>Cfg_2</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
</ItemGroup>
<Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')"/>
<ProjectExtensions>
<Borland.Personality>Delphi.Personality.12</Borland.Personality>
<Borland.ProjectType>VCLApplication</Borland.ProjectType>
<BorlandProject>
<Delphi.Personality>
<Source>
<Source Name="MainSource">rdpwrap.dpr</Source>
</Source>
<Parameters>
<Parameters Name="UseLauncher">False</Parameters>
<Parameters Name="LoadAllSymbols">True</Parameters>
<Parameters Name="LoadUnspecifiedSymbols">False</Parameters>
</Parameters>
<VersionInfo>
<VersionInfo Name="IncludeVerInfo">False</VersionInfo>
<VersionInfo Name="AutoIncBuild">False</VersionInfo>
<VersionInfo Name="MajorVer">1</VersionInfo>
<VersionInfo Name="MinorVer">0</VersionInfo>
<VersionInfo Name="Release">0</VersionInfo>
<VersionInfo Name="Build">0</VersionInfo>
<VersionInfo Name="Debug">False</VersionInfo>
<VersionInfo Name="PreRelease">False</VersionInfo>
<VersionInfo Name="Special">False</VersionInfo>
<VersionInfo Name="Private">False</VersionInfo>
<VersionInfo Name="DLL">False</VersionInfo>
<VersionInfo Name="Locale">1049</VersionInfo>
<VersionInfo Name="CodePage">1251</VersionInfo>
</VersionInfo>
<VersionInfoKeys>
<VersionInfoKeys Name="CompanyName"/>
<VersionInfoKeys Name="FileDescription"/>
<VersionInfoKeys Name="FileVersion">1.0.0.0</VersionInfoKeys>
<VersionInfoKeys Name="InternalName"/>
<VersionInfoKeys Name="LegalCopyright"/>
<VersionInfoKeys Name="LegalTrademarks"/>
<VersionInfoKeys Name="OriginalFilename"/>
<VersionInfoKeys Name="ProductName"/>
<VersionInfoKeys Name="ProductVersion">1.0.0.0</VersionInfoKeys>
<VersionInfoKeys Name="Comments"/>
</VersionInfoKeys>
<Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\bcboffice2k140.bpl">Embarcadero C++Builder Office 2000 Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\bcbofficexp140.bpl">Embarcadero C++Builder Office XP Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\dcloffice2k140.bpl">Microsoft Office 2000 Sample Automation Server Wrapper Components</Excluded_Packages>
<Excluded_Packages Name="$(BDS)\bin\dclofficexp140.bpl">Microsoft Office XP Sample Automation Server Wrapper Components</Excluded_Packages>
</Excluded_Packages>
</Delphi.Personality>
</BorlandProject>
<ProjectFileVersion>12</ProjectFileVersion>
</ProjectExtensions>
</Project>

Binary file not shown.

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
@ -17,6 +17,10 @@
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{29E4E73B-EBA6-495B-A76C-FBB462196C64}</ProjectGuid>
@ -49,8 +53,13 @@
<PlatformToolset>v120</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
</PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
@ -65,6 +74,9 @@
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>true</LinkIncremental>
@ -74,16 +86,25 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(ProjectDir)Release\x86\</OutDir>
<IntDir>$(ProjectDir)obj\Release\x86\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(ProjectDir)Release\x64\</OutDir>
<IntDir>$(ProjectDir)obj\Release\x64\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(ProjectDir)Release\arm64\</OutDir>
<IntDir>$(ProjectDir)obj\Release\arm64\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;_USRDLL;RDPWRAP_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;_USRDLL;RDPWRAP_EXPORTS;WINDOWS_IGNORE_PACKING_MISMATCH;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<SDLCheck>true</SDLCheck>
</ClCompile>
<Link>
@ -97,7 +118,7 @@
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;_USRDLL;RDPWRAP_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;_USRDLL;RDPWRAP_EXPORTS;WINDOWS_IGNORE_PACKING_MISMATCH;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<SDLCheck>true</SDLCheck>
</ClCompile>
<Link>
@ -112,7 +133,7 @@
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;RDPWRAP_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;RDPWRAP_EXPORTS;WINDOWS_IGNORE_PACKING_MISMATCH;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<SDLCheck>true</SDLCheck>
<SuppressStartupBanner>true</SuppressStartupBanner>
<WarningLevel>Level3</WarningLevel>
@ -137,7 +158,7 @@
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;RDPWRAP_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;RDPWRAP_EXPORTS;WINDOWS_IGNORE_PACKING_MISMATCH;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<SDLCheck>true</SDLCheck>
<SuppressStartupBanner>true</SuppressStartupBanner>
<WarningLevel>Level3</WarningLevel>
@ -156,6 +177,27 @@
<LinkLibraryDependencies>true</LinkLibraryDependencies>
</ProjectReference>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;RDPWRAP_EXPORTS;WINDOWS_IGNORE_PACKING_MISMATCH;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<SDLCheck>true</SDLCheck>
<SuppressStartupBanner>true</SuppressStartupBanner>
<WarningLevel>Level3</WarningLevel>
<StructMemberAlignment>1Byte</StructMemberAlignment>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<ModuleDefinitionFile>Export.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<Text Include="ReadMe.txt" />
</ItemGroup>
@ -178,6 +220,9 @@
</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
</PrecompiledHeader>
<CompileAsManaged Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">false</CompileAsManaged>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
</PrecompiledHeader>
</ClCompile>
<ClCompile Include="IniFile.cpp" />
<ClCompile Include="RDPWrap.cpp" />
@ -186,6 +231,7 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
@ -194,4 +240,4 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
</Project>

@ -1,561 +0,0 @@
RDP Wrapper Library project by Stas'M
Terminal Services supported versions
6.0.X.X (Windows Vista, any) [policy hook only]
6.0.6000.16386 (Windows Vista) [policy hook + extended patch]
6.0.6000.20723 (Windows Vista with KB944917) [todo]
6.0.6001.18000 (Windows Vista SP1) [policy hook + extended patch]
6.0.6001.22286 (Windows Vista SP1 with KB958612) [todo]
6.0.6001.22357 (Windows Vista SP1 with KB958612 v2) [todo]
6.0.6001.22323 (Windows Vista SP1 with KB960742) [todo]
6.0.6001.22392 (Windows Vista SP1 with KB968680) [todo]
6.0.6001.22565 (Windows Vista SP1 with KB977541) [todo]
6.0.6001.22635 (Windows Vista SP1 with KB970911) [todo]
6.0.6001.22801 (Windows Vista SP1 with KB2381675) [todo]
6.0.6002.18005 (Windows Vista SP2) [policy hook + extended patch]
6.0.6002.22269 (Windows Vista SP2 with KB977541) [todo]
6.0.6002.22340 (Windows Vista SP2 with KB970911) [todo]
6.0.6002.22515 (Windows Vista SP2 with KB2381675) [todo]
6.0.6002.22641 (Windows Vista SP2 with KB2523307) [todo]
6.0.6002.22790 (Windows Vista SP2 with KB2672601) [todo]
6.0.6002.19214 (Windows Vista SP2 with KB3003743 GDR) [policy hook + extended patch]
6.0.6002.23521 (Windows Vista SP2 with KB3003743 LDR) [policy hook + extended patch]
6.1.X.X (Windows 7, any) [policy hook only]
6.1.7100.0 (Windows 7 Release Candidate) [todo]
6.1.7600.16385 (Windows 7) [policy hook + extended patch]
6.1.7600.20661 (Windows 7 with KB951422) [todo]
6.1.7600.21085 (Windows 7 with KB951422 v2) [todo]
6.1.7600.20621 (Windows 7 with KB979470) [todo]
6.1.7600.20890 (Windows 7 with KB2479710) [policy hook + extended patch]
6.1.7600.21316 (Windows 7 with KB2750090) [policy hook + extended patch]
6.1.7600.21420 (Windows 7 with KB2800789) [todo]
6.1.7601.17514 (Windows 7 SP1) [policy hook + extended patch]
6.1.7601.21855 (Windows 7 SP1 with KB951422 v2) [todo]
6.1.7601.21650 (Windows 7 SP1 with KB2479710) [policy hook + extended patch]
6.1.7601.21866 (Windows 7 SP1 with KB2647409) [policy hook + extended patch]
6.1.7601.22104 (Windows 7 SP1 with KB2750090) [policy hook + extended patch]
6.1.7601.22213 (Windows 7 SP1 with KB2800789) [todo]
6.1.7601.22476 (Windows 7 SP1 with KB2870165) [todo]
6.1.7601.22435 (Windows 7 SP1 with KB2878424) [todo]
6.1.7601.22477 (Windows 7 SP1 with KB2896256) [todo]
6.1.7601.18540 (Windows 7 SP1 with KB2984972 GDR) [policy hook + extended patch]
6.1.7601.22750 (Windows 7 SP1 with KB2984972 LDR) [policy hook + extended patch]
6.1.7601.18637 (Windows 7 SP1 with KB3003743 GDR) [policy hook + extended patch]
6.1.7601.22843 (Windows 7 SP1 with KB3003743 LDR) [policy hook + extended patch]
6.1.7601.23403 (Windows 7 SP1 with KB3125574) [policy hook + extended patch]
6.1.7601.24234 (Windows 7 SP1 with KB4462923) [policy hook + extended patch]
6.2.8102.0 (Windows 8 Developer Preview) [policy hook + extended patch]
6.2.8250.0 (Windows 8 Consumer Preview) [policy hook + extended patch]
6.2.8400.0 (Windows 8 Release Preview) [policy hook + extended patch]
6.2.9200.16384 (Windows 8) [policy hook + extended patch]
6.2.9200.17048 (Windows 8 with KB2973501 GDR) [policy hook + extended patch]
6.2.9200.21166 (Windows 8 with KB2973501 LDR) [policy hook + extended patch]
6.3.9431.0 (Windows 8.1 Preview) [init hook + extended patch]
6.3.9600.16384 (Windows 8.1) [init hook + extended patch]
6.3.9600.17095 (Windows 8.1 with KB2959626) [init hook + extended patch]
6.3.9600.17415 (Windows 8.1 with KB3000850) [init hook + extended patch]
6.3.9600.18692 (Windows 8.1 with KB4022720) [init hook + extended patch]
6.3.9600.18708 (Windows 8.1 with KB4025335) [init hook + extended patch]
6.3.9600.18928 (Windows 8.1 with KB4088876) [init hook + extended patch]
6.3.9600.19093 (Windows 8.1 with KB4343891) [init hook + extended patch]
6.4.9841.0 (Windows 10 Technical Preview) [init hook + extended patch]
6.4.9860.0 (Windows 10 Technical Preview UP1) [init hook + extended patch]
6.4.9879.0 (Windows 10 Technical Preview UP2) [init hook + extended patch]
10.0.9926.0 (Windows 10 Pro Technical Preview) [init hook + extended patch]
10.0.10041.0 (Windows 10 Pro Technical Preview UP1) [init hook + extended patch]
10.0.10049.0 (Windows 10 Pro Technical Preview UP2) [todo]
10.0.10061.0 (Windows 10 Pro Technical Preview UP3) [todo]
10.0.10240.16384 (Windows 10 RTM) [init hook + extended patch]
10.0.10525.0 (Windows 10 th2_release.150812-1658) [todo]
10.0.10532.0 (Windows 10 th2_release.150822-1406) [todo]
10.0.10547.0 (Windows 10 th2_release.150913-1511) [todo]
10.0.10586.0 (Windows 10 th2_release.151029-1700) [init hook + extended patch]
10.0.10586.589 (Windows 10 th2_release.160906-1759) [init hook + extended patch]
10.0.11082.1000 (Windows 10 rs1_release.151210-2021) [init hook + extended patch]
10.0.11102.1000 (Windows 10 rs1_release.160113-1800) [init hook + extended patch]
10.0.14251.1000 (Windows 10 rs1_release.160124-1059) [init hook + extended patch]
10.0.14271.1000 (Windows 10 rs1_release.160218-2310) [init hook + extended patch]
10.0.14279.1000 (Windows 10 rs1_release.160229-1700) [init hook + extended patch]
10.0.14295.1000 (Windows 10 rs1_release.160318-1628) [init hook + extended patch]
10.0.14300.1000 (Windows Server 2016 Technical Preview 5) [init hook + extended patch]
10.0.14316.1000 (Windows 10 rs1_release.160402-2227) [init hook + extended patch]
10.0.14328.1000 (Windows 10 rs1_release.160418-1609) [init hook + extended patch]
10.0.14332.1001 (Windows 10 rs1_release.160422-1940) [init hook + extended patch]
10.0.14342.1000 (Windows 10 rs1_release.160506-1708) [init hook + extended patch]
10.0.14352.1002 (Windows 10 rs1_release.160522-1930) [init hook + extended patch]
10.0.14366.0 (Windows 10 rs1_release.160610-1700) [init hook + extended patch]
10.0.14367.0 (Windows 10 rs1_release.160613-1700) [init hook + extended patch]
10.0.14372.0 (Windows 10 rs1_release.160620-2342) [init hook + extended patch]
10.0.14379.0 (Windows 10 rs1_release.160627-1607) [init hook + extended patch]
10.0.14383.0 (Windows 10 rs1_release.160701-1839) [init hook + extended patch]
10.0.14385.0 (Windows 10 rs1_release.160706-1700) [init hook + extended patch]
10.0.14388.0 (Windows 10 rs1_release.160709-1635) [init hook + extended patch]
10.0.14393.0 (Windows 10 rs1_release.160715-1616) [init hook + extended patch]
10.0.14393.1198 (Windows 10 rs1_release_sec.170427-1353) [init hook + extended patch]
10.0.14393.1737 (Windows 10 rs1_release_inmarket.170914-1249) [init hook + extended patch]
10.0.14393.2457 (Windows 10 rs1_release_inmarket.180822-1743) [init hook + extended patch]
10.0.14901.1000 (Windows 10 rs_prerelease.160805-1700) [init hook + extended patch]
10.0.14905.1000 (Windows 10 rs_prerelease.160811-1739) [init hook + extended patch]
10.0.14915.1000 (Windows 10 rs_prerelease.160826-1902) [init hook + extended patch]
10.0.14926.1000 (Windows 10 rs_prerelease.160910-1529) [init hook + extended patch]
10.0.14931.1000 (Windows 10 rs_prerelease.160916-1700) [init hook + extended patch]
10.0.14936.1000 (Windows 10 rs_prerelease.160923-1700) [init hook + extended patch]
10.0.14942.1000 (Windows 10 rs_prerelease.161003-1929) [init hook + extended patch]
10.0.14946.1000 (Windows 10 rs_prerelease.161007-1700) [init hook + extended patch]
10.0.14951.1000 (Windows 10 rs_prerelease.161014-1700) [init hook + extended patch]
10.0.14955.1000 (Windows 10 rs_prerelease.161020-1700) [init hook + extended patch]
10.0.14959.1000 (Windows 10 rs_prerelease.161026-1700) [init hook + extended patch]
10.0.14965.1001 (Windows 10 rs_prerelease.161104-1700) [init hook + extended patch]
10.0.14971.1000 (Windows 10 rs_prerelease.161111-1700) [init hook + extended patch]
10.0.14986.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.14997.1001 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.15002.1001 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.15007.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.15014.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.15019.1000 (Windows 10 rs_prerelease.170121-1513) [init hook + extended patch]
10.0.15025.1000 (Windows 10 rs_prerelease.170127-1750) [init hook + extended patch]
10.0.15031.0 (Windows 10 rs2_release.170204-1546) [init hook + extended patch]
10.0.15042.0 (Windows 10 rs2_release.170219-2329) [init hook + extended patch]
10.0.15046.0 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.15048.0 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.15055.0 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.15058.0 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.15061.0 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.15063.0 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.15063.296 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.15063.994 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.15063.1155 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16179.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16184.1001 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16199.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16215.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16232.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16237.1001 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16241.1001 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16251.0 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16251.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16257.1 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16257.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16273.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16275.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16278.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16281.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16288.1 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16291.0 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16294.1 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16296.0 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16299.0 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16299.15 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16353.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.16362.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.17004.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.17017.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.17025.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.17035.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.17040.1000 (Windows 10 WinBuild.160101.0800) [todo]
10.0.17046.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.17063.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.17074.1002 (Windows 10 WinBuild.160101.0800) [todo]
10.0.17083.1000 (Windows 10 WinBuild.160101.0800) [todo]
10.0.17115.1 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.17128.1 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.17133.1 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.17134.1 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.17723.1000 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
10.0.17763.1 (Windows 10 WinBuild.160101.0800) [init hook + extended patch]
Source code changelog (rdpwrap library):
2018.10.10 :
- added support for termsrv.dll 6.1.7601.24234 x86
2018.10.04 :
- added support for termsrv.dll 10.0.14393.2457 x86
2018.10.03 :
- added support for termsrv.dll 6.1.7601.24234 x64
- added support for termsrv.dll 10.0.15063.994 x64
- added support for termsrv.dll 10.0.17723.1000 x64
- added support for termsrv.dll 10.0.17763.1
2018.09.10 :
- added support for termsrv.dll 6.1.7600.20890
- added support for termsrv.dll 6.1.7600.21316
- added support for termsrv.dll 6.1.7601.21650
- added support for termsrv.dll 6.1.7601.21866
- added support for termsrv.dll 6.1.7601.22104
- added support for termsrv.dll 6.3.9600.19093
- added support for termsrv.dll 10.0.14393.2457 x64
- added support for termsrv.dll 10.0.15063.1155 x64
2018.05.16 :
- added support for termsrv.dll 10.0.17115.1
- added support for termsrv.dll 10.0.17128.1
- added support for termsrv.dll 10.0.17133.1
- added support for termsrv.dll 10.0.17134.1
2018.03.26 :
- added support for termsrv.dll 6.3.9600.18928 by 1nd1g0
2017.12.27 :
- added support for termsrv.dll 10.0.17017.1000
- added support for termsrv.dll 10.0.17025.1000
- added support for termsrv.dll 10.0.17035.1000
- added support for termsrv.dll 10.0.17046.1000
- added support for termsrv.dll 10.0.17063.1000
2017.10.13 :
- added support for termsrv.dll 10.0.14393.1737
- added support for termsrv.dll 10.0.16299.0
- added support for termsrv.dll 10.0.16299.15
- added support for termsrv.dll 10.0.17004.1000
2017.09.24 :
- added support for termsrv.dll 10.0.16291.0
- added support for termsrv.dll 10.0.16294.1
- added support for termsrv.dll 10.0.16296.0
- added support for termsrv.dll 10.0.16362.1000
2017.09.15 :
- added support for termsrv.dll 10.0.16288.1
2017.09.06 :
- added support for termsrv.dll 10.0.16273.1000
- added support for termsrv.dll 10.0.16275.1000
- added support for termsrv.dll 10.0.16278.1000
- added support for termsrv.dll 10.0.16281.1000
- added support for termsrv.dll 10.0.16353.1000
2017.08.04 :
- added support for termsrv.dll 10.0.16257.1
- added support for termsrv.dll 10.0.16257.1000
2017.07.30 :
- added support for termsrv.dll 6.3.9600.18708
- added support for termsrv.dll 10.0.16232.1000
- added support for termsrv.dll 10.0.16237.1001
- added support for termsrv.dll 10.0.16241.1001
- added support for termsrv.dll 10.0.16251.0
- added support for termsrv.dll 10.0.16251.1000
2017.06.29 :
- added support for termsrv.dll 6.3.9600.18692
2017.06.10 :
- added support for termsrv.dll 10.0.15063.296
- added support for termsrv.dll 10.0.16215.1000
2017.05.29 :
- added support for termsrv.dll 10.0.16199.1000
2017.05.17 :
- added support for termsrv.dll 10.0.14997.1001 x64
2017.05.12 :
- added support for termsrv.dll 10.0.14393.1198 x86
2017.05.03 :
- added support for termsrv.dll 10.0.16179.1000
- added support for termsrv.dll 10.0.16184.1001
2017.03.22 :
- added support for termsrv.dll 10.0.15063.0
2017.03.21 :
- added support for termsrv.dll 10.0.15061.0
2017.03.16 :
- added support for termsrv.dll 10.0.15058.0
2017.03.14 :
- added support for termsrv.dll 10.0.15055.0
2017.03.05 :
- added support for termsrv.dll 10.0.15048.0
2017.03.02 :
- added support for termsrv.dll 10.0.15046.0
2017.03.01 :
- added support for termsrv.dll 10.0.15031.0
- added support for termsrv.dll 10.0.15042.0
2017.02.03 :
- added support for termsrv.dll 10.0.15025.1000 x64
2017.01.28 :
- added support for termsrv.dll 10.0.15019.1000
2017.01.21 :
- added support for termsrv.dll 10.0.15014.1000
2017.01.15 :
- added support for termsrv.dll 10.0.15007.1000
2017.01.12 :
- added support for termsrv.dll 10.0.15002.1001
2016.12.23 :
- added support for termsrv.dll 10.0.14986.1000
2016.11.19 :
- added support for termsrv.dll 10.0.14959.1000
- added support for termsrv.dll 10.0.14965.1001
- added support for termsrv.dll 10.0.14971.1000
2016.10.28 :
- added support for termsrv.dll 10.0.14955.1000
2016.10.21 :
- added support for termsrv.dll 10.0.14951.1000
2016.10.19 :
- added support for termsrv.dll 10.0.14946.1000
2016.10.08 :
- added support for termsrv.dll 10.0.14942.1000
2016.09.30 :
- added support for termsrv.dll 10.0.14936.1000
2016.09.27 :
- added support for termsrv.dll 10.0.14931.1000
2016.09.15 :
- added support for termsrv.dll 10.0.14926.1000
2016.09.14 :
- added support for termsrv.dll 10.0.10586.589
2016.09.03 :
- added support for termsrv.dll 10.0.14915.1000
2016.08.28 :
- added support for termsrv.dll 6.1.7601.23403
- added support for termsrv.dll 10.0.14901.1000
- added support for termsrv.dll 10.0.14905.1000
2016.08.12 :
- added support for termsrv.dll 10.0.14385.0
2016.08.01 :
- preparing the release
2016.07.23 :
- added online install mode to installer
- added feature to keep settings on uninstall
- fixed update firewall rule on port change in config tool
- added feature to hide users on logon
2016.07.22 :
- added support for termsrv.dll 10.0.14393.0
2016.07.15 :
- added support for termsrv.dll 10.0.14383.0
- added support for termsrv.dll 10.0.14388.0
2016.07.06 :
- added support for termsrv.dll 10.0.14379.0
2016.06.27 :
- added support for termsrv.dll 10.0.14372.0 x86
2016.06.26 :
- added support for termsrv.dll 10.0.14372.0 x64 by kbmorris
2016.06.17 :
- fixed issue with termsrv.dll 10.0.14352.1002
- added support for termsrv.dll 10.0.14366.0
- added support for termsrv.dll 10.0.14367.0
2016.05.30 :
- added support for termsrv.dll 10.0.14352.1002
2016.05.14 :
- added support for termsrv.dll 10.0.14342.1000
2016.05.08 :
- added support for termsrv.dll 10.0.14300.1000 x64
- added support for termsrv.dll 10.0.14328.1000
2016.04.29 :
- added support for termsrv.dll 10.0.14332.1001 by maxpiva
2016.04.14 :
- added support for termsrv.dll 10.0.14316.1000
2016.04.06 :
- added support for termsrv.dll 10.0.14295.1000
2016.03.07 :
- added experimental codes for ARMv7 architecture (see rdpwrap-arm-kb.ini)
- Windows RT / termsrv.dll 6.2.9200.16384
- Windows RT 8.1 / termsrv.dll 6.3.9600.16384
- Windows RT 8.1 / termsrv.dll 6.3.9600.17095
2016.03.06 :
- added support for termsrv.dll 10.0.14279.1000
2016.02.29 :
- added support for termsrv.dll 10.0.14271.1000
2016.01.28 :
- added support for termsrv.dll 10.0.14251.1000
2016.01.26 :
- added support for termsrv.dll 10.0.11102.1000
2016.01.15 :
- updated messages in the installer
- added support for termsrv.dll 10.0.11082.1000
2015.11.14 :
- added support for termsrv.dll 10.0.10586.0
2015.08.11 :
- embed new rdpclip versions in the installer (for NT 6.0 and 6.1)
- preparing the release
2015.08.07 :
- added INI update feature to installer
2015.07.30 :
- fixed issue with Windows 10 Home x86 (wrong LocalOnly offset was specified in INI file)
2015.07.17 :
- added support for termsrv.dll 10.0.10240.16384
- added HOW TO hints to KB (so other reverse engineers can do this hard work more easier)
2015.07.16 :
- moved all comments from INI file to Knowledge Base text file
- now INI file have smaller size
- updated RDP checker: changed IP Address to 127.0.0.2 (sometimes client doesn't want to connect .1), updated text message
- updated RDP config: list all possible shadowing modes, also write group policy
- updated installer: added workaround for 1056 error
- updated copyright years in source code
- obtained files from build 10.0.10240.16384
- researching Windows 10 RTM
2015.03.23 :
- researching Windows 10 Pro Technical Preview UP1
- added support for termsrv.dll 10.0.10041.0
2015.03.20 :
- new build 10.0.10041.0 was released, obtaining files...
2015.01.26 :
- researching Windows 10 Pro Technical Preview (10.0.9926.0 x86)
- added support for termsrv.dll 10.0.9926.0 (x86)
2015.01.22 :
- v-yadli contributed offsets for version 10.0.9926.0 (x64)
2014.12.13 :
- added more policy values to INI file
2014.12.10 :
- C++ version seems to work well now!
- added support for termsrv.dll 6.4.9879.0
- preparing the new release
2014.12.09 :
- many bug fixes in C++ version, you can track it in the git history :)
- it can be compiled now :D
- we are getting closer to the finish line!
2014.12.03 :
- added INI reader by Fusix for C++ version
- asulwer also helped with the development
2014.11.25 :
- corrected some typos in INI file
- added EasyPrint policy value
2014.11.24 :
- added support for termsrv.dll 6.3.9600.17415
2014.11.21 :
- new LiteINI module to read INI files
- added support to store patch settings in INI file
- version support can be extended without recompilation
- C++ version needs to be updated
2014.11.20 :
- improved comments
- researching KB3000850
- found required files
- improving RDPWrap...
- placing signatures, offsets, values, etc in separate config file
- working with code
2014.11.13 :
- researching KB3003743
- added support for version 6.0.6002.19214
- added support for version 6.0.6002.23521
- added support for version 6.1.7601.18637
- added support for version 6.1.7601.22843
2014.11.02 :
- researching termsrv.dll 6.4.9860.0
- done
2014.10.19 :
- added support for version 6.0.6000.16386 (x64)
- added support for version 6.0.6001.18000 (x64)
- added support for version 6.1.7600.16385
2014.10.18 :
- corrected some typos in source
- simplified signature constants
- added support for version 6.0.6000.16386 (x86)
- added support for version 6.0.6001.18000 (x86)
- added support for version 6.0.6002.18005
- added support for version 6.1.7601.17514
- added support for version 6.1.7601.18540
- added support for version 6.1.7601.22750
- added support for version 6.2.9200.17048
- added support for version 6.2.9200.21166
2014.10.17 :
- collecting information about all versions of Terminal Services beginning from Vista
- added [todo] to the versions list
2014.10.16 :
- got new updates: KB2984972 for Win 7 (still works with 2 concurrent users) and KB2973501 for Win 8 (doesn't work)
2014.10.02 :
- researching Windows 10 TP Remote Desktop
- done! even without debugging symbols ^^)
2014.07.20 :
- added support for Windows 8 Release Preview
- added support for Windows 8 Consumer Preview
- added support for Windows 8 Developer Preview
2014.07.19 :
- improved patching of Windows 8
- added policy patches
- will patch CDefPolicy::Query
- will patch CSessionArbitrationHelper::IsSingleSessionPerUserEnabled
2014.07.18 :
- researched patched files from MDL forum
- CSLQuery::GetMaxSessions requires no patching
- it's better to change the default policy, so...
- will patch CDefPolicy::Query
- will patch CEnforcementCore::GetInstanceOfTSLicense
- will patch CSessionArbitrationHelper::IsSingleSessionPerUserEnabled
- the function CSLQuery::Initialize is hooked correctly
2014.07.17 :
- will hook only CSLQuery::Initialize function
- CSLQuery::GetMaxSessions will be patched
- added x86 signatures for 6.3.9431.0 (Windows 8.1 Preview)
2014.07.16 :
- changing asm opcodes is bad, will hook CSL functions
2014.07.15 :
- added x86 signatures for 6.3.9600.16384 (Windows 8.1)
2014.07.15 :
- added x86 signatures for 6.3.9600.17095 (Windows 8.1 with KB2959626)

@ -1,35 +0,0 @@
# RDPWrapOffsetFinder
Pre-built binaries of [llccd/RDPWrapOffsetFinder](https://github.com/llccd/RDPWrapOffsetFinder),
committed here so that the CI pipeline and releases are self-contained and
reproducible without depending on an external release being available at build time.
## Contents
```
x64/
RDPWrapOffsetFinder.exe # x86-64 build
Zydis.dll # required runtime (x64)
x86/
RDPWrapOffsetFinder.exe # x86 32-bit build
Zydis.dll # required runtime (x86)
VERSION # version tag of the upstream release
```
## Usage
Extract the appropriate arch folder and run:
```
.\RDPWrapOffsetFinder.exe C:\Windows\System32\termsrv.dll
```
The output `[10.0.xxxxx.xxxxx]` section can be appended to `res/rdpwrap.ini`
and submitted as a pull request.
## Updating
To update the binaries when llccd releases a new version, trigger the
`Update RDPWrapOffsetFinder tools` workflow from the Actions tab
(`.github/workflows/update-finder-tools.yml`).
It will download the latest release, update this folder, and open a pull request.

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save